修饰局部变量
#include
void test()
{
int i = 0;//不加static结果10个1
//static int i = 0;//加了结果10个0
i++;
printf("%d ", i);
}
int main()
{
int i = 0;
for(i=0; i<10; i++)
{
test();c
}
return 0;
}
修饰全局变量
被修饰的全局变量只能在本源文件中使用,不能在其他源文件中使用
修饰函数
被修饰的函数只能在本源文件中使用,不能在其他源文件中使用
else与离他最近的if匹配
//请问循环要循环多少次?
#include
int main()
{
int i = 0;
int k = 0;
for(i =0,k=0; k=0; i++,k++)
k++;
return 0;
}
答案是0次;因为循环条件k=0;0为假,非0为真,所以条件不满足不进入循环,把k改成任何非0的数,都会死循环,因为该完之后条件一直为真。
绝大多数情况下,数组名表示数组首元素的地址,以下两种情况除外
左移操作符 <<:补0;
右移操作符 >>:
算数右移:补符号位 (vs编译器就是采用的算数右移)
逻辑右移:补0
这里考察的是异或操作符,我们需要知道一下两点:
//方法1:只适合正数,因为负数右移补符号位补1
int main()
{
int num = 100;
int count = 0;
while (num)
{
if (num & 1 == 1)
{
count++;
}
num >>= 1;
}
printf("%d", count);
return 0;
}
//方法2
int main()
{
int num = 10;
int i = 0;
int count = 0;//计数
for(i=0; i<32; i++)
{
if( num & (1 << i) )
count++;
}
printf("二进制中1的个数 = %d\n",count);
return 0;
}
//思考还能不能更加优化,这里必须循环32次的。
//方法3
int main()
{
int num = -1;
int i = 0;
int count = 0;//计数
while(num)
{
count++;
num = num&(num-1);
}
printf("二进制中1的个数 = %d\n",count);
return 0;
}
表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
提升规则:
例如char a = 2; char b = -1; 在 char c = a+b;这条语句中。a和b首先要经过整形提升,a在内存中是00000010,提升之后变成00000000 00000000 00000000 00000010,b在内存中是11111111,整形提升之后变成11111111 11111111 11111111 11111111,相加之后变成00000000 00000000 00000000 00000001,在阶段低八位00000001赋给c。
根据国际标准IEEE(电气和电子工程协会) 754,任意一个二进制浮点数V可以表示成下面的形式:
对于有效数字M和指数E,还有一些特殊规定:
对于有效数字M:因为M大于等于1小于2,也就是说M始终是一个1.xxxxx的数,所以可以将1省略,只保存xxxxxx部分。如保存1.01时,可以只保存01,等到读取的时候,再把省略掉的1加上去。这样一来,以32位浮点数为例,原本留给M的有23位,省略第一位的1之后,可以多保存一位小数点后面的有效数字。
对于指数E:首先E是一个符号整数。32位的浮点数,E为8位,范围0-255,64位的浮点数,E为11位,范围0-2047。可是如果E是负数怎么表示呢?
以上是把E存进去的过程。下面我们来讨论一下把E从内存中取出来的过程
E在内存中为全0
这说明E的真实值是-127,1.xxx * 2^(-127)这个数趋近于0.
E在内存中为全1
说明E的真实值是128,1.xxx * 2^(128),这个数加上正负号分别对应正负无穷。
E不为全1或全0
内存中保存的值-127即为真实值。
有了以上的了解,我们来看这样一道题。
int main()
{
int n = 9;
float *pFloat = (float *)&n;
printf("n的值为:%d\n",n);
printf("*pFloat的值为:%f\n",*pFloat);
*pFloat = 9.0;
printf("num的值为:%d\n",n);
printf("*pFloat的值为:%f\n",*pFloat);
return 0;
}
//这段程序打印的结果是多少?
//代码1
(*(void (*)())0)();
//首先 void (*) () 这是一个函数指针,函数的参数为无参,返回值为void类型。
//我们知道(类型)是强制类型转换,所以这里是将0强制转换为函数指针类型。
//(*(void (*)())0)(),在将这个函数指针类型解引用相当于调用这个函数,由于函数的参数类型为无参,所以最后一对括号里面是空的
//代码2
void (*signal(int , void(*)(int)))(int);
//首先signal肯定是与括号先结合,说明signal是一个函数
//signal(int, void(*)(int)) 说明signal函数的第一个参数是int,第二个参数是void(*)(int),这个一个函数指针类型。
//我们把函数名和函数参数拿走,剩下的东西就是函数的返回值,我们发现剩下的是void(*)(int),也就是说signal函数的返回值是一个函数指针
//所以,这行代码是一个函数声明,函数名是signal,函数有两个参数,第一个参数是int,第二个参数是函数指针,函数的返回值也是一个函数指针
//我们可以对上面的代码做简化,方便我们看清
typedef void(*func)(int);
func signal(int, func);
const char * strstr ( const char * str1, const char * str2 );
用来判断字符串str2是不是str1的子串。该函数返回字符串2第一次出现在字符串中的位置,如果字符串2不是字符串1的一个子串,则返回空指针。
strtok
char * strtok ( char * str, const char * delimiters );
这是一个字符串分割函数。第一个参数是要分割的字符串,第二个参数是分隔符。如果str不为空指针,则从str的起始位置开始遍历找分隔符,找到第一个分隔符的时候,会将该位置记录下来,并将该分隔符修改成’\0‘,并返回该子串。若str为空指针,则从str的上次保存的分隔符位置开始向后遍历,直到找到分隔符,再次记录该位置,修改该分隔符为’\0‘,返回该子串。如果遍历字符串结束,则返回空指针。说的比较绕口,下面我们举一个例子帮助大家理解一下。
例:假如我们要分割这样一个字符串 1411482854@qq.com,以@和.作为分隔符,将该字符串分割3部分,我们可以这样做
int main()
{
char str[] = "1411482854@qq.com";
char* sep = "@.";//这里保存分隔符。
char* substr = NULL;
substr = strtok(str, sep);
printf("%s\n", substr);
substr = strtok(NULL, sep);
printf("%s\n", substr);
substr = strtok(NULL, sep);
printf("%s\n", substr);
return 0;
}
这种方式虽然能正确打印出结果,但是,这是我们知道这个字符串要被分成3部分,要调用3次。可是,如果要分割的字符串很长的时候我们该怎么办呢?我们可以通过循环的方式来解决,请看下面的代码
int main()
{
char str[] = "1411482854@qq.com";
char* sep = "@.";//这里保存分隔符。
char* substr = NULL;
//for (substr = strtok(str, sep); substr != NULL; substr = strtok(NULL, sep))
//{
// printf("%s\n", substr);
//}
substr = strtok(str, sep);
while (substr != NULL)
{
printf("%s\n", substr);
substr = strtok(NULL, sep);
}
return 0;
}
for循环和while循环都可以解决这个问题,这里还要提一个小细节。如果代码写成下面这样,程序就会报错
int main()
{
//char str[] = "1411482854@qq.com";
char* str = "1411482854@qq.com";
char* sep = "@.";//这里保存分隔符。
char* substr = NULL;
//for (substr = strtok(str, sep); substr != NULL; substr = strtok(NULL, sep))
//{
// printf("%s\n", substr);
//}
substr = strtok(str, sep);
while (substr != NULL)
{
printf("%s\n", substr);
substr = strtok(NULL, sep);
}
return 0;
}
我们仔细观察,函数的第一个参数,没有带const, 并且该函数要对原字符串进行修改。这种定义str的方式,str是一个指针,指向的字符常量区,字符常量区的内容是不允许被修改的。所以就会报错。而如果我们定义char str[],这其实是在栈上开辟一段空间,并将字符常量区的数据拷贝到字符数组,而字符数组里面的内容是可以被修改的,因此不会报错。同时,由于该函数会修改原字符串,所以有些情况下,我们需要将原字符串拷贝一下,再对拷贝的字符串使用该函数。
strerror
char * strerror ( int errnum );
//返回错误码,所对应的错误信息。
memcpy
void * memcpy ( void * destination, const void * source, size_t num );
memmove
void * memmove ( void * destination, const void * source, size_t num );
这个函数专门来解决memcpy中destina和source重叠的问题,能够确保source所在重叠区域在拷贝之前不被覆盖
memcmp
int memcmp ( const void * ptr1, const void * ptr2, size_t num );
比较从ptr1和ptr2指针开始的num个字节.和strcmp类似,但是不同的是这个是以字节为单位比较。
模拟实现strlen
//方法1:计数器版
int my_strlen(const char* str)
{
int count = 0;
while (*str != '\0')
{
count++;
str++;
}
return count;
}
//方法2:不创建计数器count变量 递归实现
int my_strlen(const char* str)
{
if (*str == '\0')
{
return 0;
}
return 1 + my_strlen(str + 1);
}
//方法3:指针相减
int my_strlen(const char* str)
{
char* cur = str;
while (*cur)
{
cur++;
}
return cur - str;
}
模拟实现strcpy
char* my_strcpy(char* destination, const char* source)
{
assert(destination && source);
char* start = destination;
while (*source != '\0')
{
*destination++ = *source++;
}
return start;
}
//这种写法有点小问题,因为当source到结尾'\0'的时候,调出了循环,没有将'\0'赋给destination
//应该这样修改
char* my_strcpy(char* destination, const char* source)
{
assert(destination && source);
char* start = destination;
while (*destination++ = *source++)
{}
return start;
}
模拟实现strcat
char* my_strcat(char* destination, const char* source)
{
assert(destination && source);
char* start = destination;
while (*destination)
{
destination++;
}
while(*destination++ = *source++)
{}
return start;
}
模拟实现strstr
char* my_strstr(const char* str1, const char* str2)
{
assert(str1 && str2);
if (*str2 == '\0')
{
//如果str2是空串,直接返回str1
return str1;
}
const char* s1 = str1;
const char* s2 = str2;
const char* cur = str1;//记录有可能是子串的起始位置
while (*cur)
{
if (*s1 != *s2)
{
cur++;
s1 = cur;
s2 = str2;
}
else
{
s1++;
s2++;
if (!*s2)
{
return cur;
}
}
}
return NULL;
}
模拟实现strcmp
int my_strcmp(const char* str1, const char* str2)
{
assert(str1 && str2);
while (*str1 || *str2)
{
if (*str1 > *str2)
{
return 1;
}
else if (*str1 < *str2)
{
return -1;
}
else
{
str1++;
str2++;
}
}
return 0;
}
模拟实现memcpy
void* my_memcpy(void* dest, void* src, int num)
{
assert(dest && src);
//因为是按字节拷贝的,而一个char类型的大小是一个字节,所以先强制类型转换为char*
char* s1 = (char*)dest;
char* s2 = (char*)src;
while (num--)
{
*s1++ = *s2++;
}
return dest;
}
模拟实现memmove
void* my_memmove(void* dest, void* src, int num)
{
assert(dest && src);
//如果是高地址向低地址拷贝,那么和my_memcpy思路一样
//如果是低地址向高地址拷贝,此时如果dest和src右重叠部分,我们要我们让dest和src都向后走num-1,然后倒着拷贝。若没有重叠部分,则和my_memcpy思路一样
//所以我们分两种情况:1.需要保存重叠部分。 2.不需要保存重叠部分
char* s1 = (char*)dest;
char* s2 = (char*)src;
if (s2 + num > s1)
{
s2 = s2 + num - 1;
s1 = s1 + num - 1;
while (num--)
{
*s1-- = *s2--;
}
}
else
{
while (num--)
{
*s1++ = *s2++;
}
}
return dest;
}
对齐规则:
第一个成员在与结构体变量偏移量为0的地址处。
其他成员变量要对齐到对齐数的整数倍的地址处。
对齐数 = min(编译器的默认对齐数, 该成员大小);VS2022中默认对齐数为8
结构体总大小为最大对齐数的整数倍。
若结构体内嵌套了结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的大小就是所有最大对齐数的整数倍。
下面我们来举例说明一下:(我们这里认为默认对齐数为8)
struct S1
{
char a;
int b;
long long c;
};
位段的声明和结构体相似,不过有两个不同点。
如:
struct S
{
int a : 30;
short b : 10;
char c : 4;
};
位段的大小如何计算呢?我们就以上面的结构体为例,说明一下
位段虽然可以节省空间,但是有许多弊端:
这些问题导致位段的可移植性很差。
枚举就是一一列举的意思。有些东西我们是可以枚举的。比如星期,只有星期一到星期日。在比如性别,要么是男要么是女。
enum Day
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
这些枚举的可能取值都是有值的,默认从0开始,一次递增1.当然在定义的时候可以赋初值。如
enum Color//颜色
{
RED=1,
GREEN=2,
BLUE=4
};
如果枚举个某一个可能取值赋初值了,而他下面的一个可能取值未赋初值,那么下面的可能取值对应得值就是赋初值的值加1.此外,假如上面的赋初值为5,下面也可以赋初值为3,不一定要比5大才可以,甚至也赋值为5也不会报错。
union Un
{
int i;
char c;
};
int main()
{
union Un u;
printf("%d\n", sizeof(u));
printf("%p\n", &u);
printf("%p\n", &u.i);
printf("%p\n", &u.c);
return 0;
}
这段代码的运行结果如上。我们发现三个地址一样,并且大小为4个字节。所以在内存中,i和c是公用一块空间的,因此联合体又成共用体。联合体的大小=成员中最大的大小,因为至少要存的下最大的那个成员。
在C99中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员。例如:
struct S
{
int i;
int arr[0];//这就是柔性数组
}
//像上面这样写有的编译器会报错,要这样改
struct S
{
int i;
int arr[];//这就是柔性数组
}
注意:
文件操作的常用函数
打开/关闭文件
//打开文件
FILE * fopen ( const char * filename, const char * mode );
//关闭文件
int fclose ( FILE * stream );
打开方式如下:
文件打开方式 | 含义 | 如果指定文件不存在 |
---|---|---|
“r”(只读) | 为了读取数据,打开一个已经存在的文本文件 | 出错 |
“w”(只写) | 为了写入数据,打开一个文本文件 | 建立一个新的文件 |
“a”(追加) | 向文本文件尾添加数据 | 建立一个新的文件 |
“rb”(只读) | 为了读取数据,打开一个二进制文件 | 出错 |
“wb”(只写) | 为了写入数据,打开一个文本文件 | 建立一个新的文件 |
“ab”(追加) | 向一个二进制文件尾添加数据 | 建立一个新的文件 |
“r+”(读写) | 为了读和写,打开一个文本文件 | 出错 |
“w+”(读写) | 为了读和写,打开一个文本文件 | 建立一个新的文件 |
“a+”(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
“rb+”(读写) | 为了读和写打开一个二进制文件 | 出错 |
“wb+”(读写) | 为了读和写,打开一个二进制文件 | 建立一个新的文件 |
“ab+”(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
文件的顺序读写函数:
功能 | 函数名 | 适用于 |
---|---|---|
字符输入函数 | fgetc | 所有输入流 |
字符输出函数 | fputc | 所有输出流 |
文本行输入函数 | fgets | 所有输入流 |
文本行输出函数 | fputs | 所有输出流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输出流 |
二进制输入 | fwrite | 文件 |
二进制输出 | fread | 文件 |
下面我们对比一下scanf/fscanf/sscanf 和 printf/fprintf/sprintf
scanf/fscanf/sscanf
printf/fprintf/sprintf
文件的随机读写函数
fseek
int fseek ( FILE * stream, long int offset, int origin );
//offset为偏移量,origin为文件指针的其实位置。该函数就是通过起始位置加偏移量的方式来指定位置对文件进行读写操作。其中origin有三个选项,分别表示文件起始位置文件指针当前位置,文件结尾。
ftell
long int ftell ( FILE * stream );
//Get current position in stream 返回文件指针相对于起始位置的偏移量
rewind
void rewind ( FILE * stream );
//Set position of stream to the beginning 让文件指针的位置回到文件的起始位置
文件读取结束的判定