1.转义字符\ddd 和 \xdd
2.关键字static
-
修饰局部变量
#include <stdio.h> 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; }
-
修饰全局变量
被修饰的全局变量只能在本源文件中使用,不能在其他源文件中使用
-
修饰函数
被修饰的函数只能在本源文件中使用,不能在其他源文件中使用
3if else语句注意
else与离他最近的if匹配
4.for循环的坑
//请问循环要循环多少次?
#include <stdio.h>
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的数,都会死循环,因为该完之后条件一直为真。
5.数组名的含义
绝大多数情况下,数组名表示数组首元素的地址,以下两种情况除外
- sizeof(数组名),表示整个数组的大小
- &数组名,取出的是整个数组的地址,此时数组名表示的是整个数组
6.移位操作符
-
左移操作符 <<:补0;
-
右移操作符 >>:
算数右移:补符号位 (vs编译器就是采用的算数右移)
逻辑右移:补0
7.不创建新的变量实现两个数的交换
这里考察的是异或操作符,我们需要知道一下两点:
- 任何数与自身异或结果为0;
- 0异或任何数结果为任何数;
8.统计一个数二进制中1的个数
//方法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;
}
9.整形提升
表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
提升规则:
- 无符号数:高位补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。
10.浮点数在内存中的存储
根据国际标准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; } //这段程序打印的结果是多少?
11.两段有趣的代码
//代码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);
12.几个字符串相关函数
-
strstr
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 );
- 函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置。
- 这个函数在遇到 ‘\0’ 的时候并不会停下来。
- source和destin所指的内存区域可能重叠,这个函数不能够确保source所在重叠区域在拷贝之前不被覆盖
- 这个函数可以拷贝任意类型的数据,而strcpy只能拷贝字符串。通常拷贝字符串时用strcpy,其他时候拷贝用memcpy
-
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类似,但是不同的是这个是以字节为单位比较。
13常用库函数的模拟实现
-
模拟实现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; }
14.结构体内存对齐
对齐规则:
-
第一个成员在与结构体变量偏移量为0的地址处。
-
其他成员变量要对齐到对齐数的整数倍的地址处。
对齐数 = min(编译器的默认对齐数, 该成员大小);VS2022中默认对齐数为8
-
结构体总大小为最大对齐数的整数倍。
-
若结构体内嵌套了结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的大小就是所有最大对齐数的整数倍。
下面我们来举例说明一下:(我们这里认为默认对齐数为8)
struct S1
{
char a;
int b;
long long c;
};
15.位段
位段的声明和结构体相似,不过有两个不同点。
- 位段的成员类型必须是int,short,或char类型
- 位段的成员后面有一个冒号和一个数字
如:
struct S
{
int a : 30;
short b : 10;
char c : 4;
};
位段的大小如何计算呢?我们就以上面的结构体为例,说明一下
位段虽然可以节省空间,但是有许多弊端:
- int 位段被当成有符号数还是无符号数是不确定的
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机
器会出问题。 - 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是
舍弃剩余的位还是利用,这是不确定的。
这些问题导致位段的可移植性很差。
16.枚举
枚举就是一一列举的意思。有些东西我们是可以枚举的。比如星期,只有星期一到星期日。在比如性别,要么是男要么是女。
enum Day
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
这些枚举的可能取值都是有值的,默认从0开始,一次递增1.当然在定义的时候可以赋初值。如
enum Color//颜色
{
RED=1,
GREEN=2,
BLUE=4
};
如果枚举个某一个可能取值赋初值了,而他下面的一个可能取值未赋初值,那么下面的可能取值对应得值就是赋初值的值加1.此外,假如上面的赋初值为5,下面也可以赋初值为3,不一定要比5大才可以,甚至也赋值为5也不会报错。
17.联合
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是公用一块空间的,因此联合体又成共用体。联合体的大小=成员中最大的大小,因为至少要存的下最大的那个成员。
18.柔性数组
在C99中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员。例如:
struct S
{
int i;
int arr[0];//这就是柔性数组
}
//像上面这样写有的编译器会报错,要这样改
struct S
{
int i;
int arr[];//这就是柔性数组
}
注意:
- 柔性数组成员前面必须至少有一个其他类型成员
- sizeof计算结构大小的时候不包括柔性数组
- 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大
小,以适应柔性数组的预期大小。
19.C语言文件操作
文件操作的常用函数
-
打开/关闭文件
//打开文件 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 让文件指针的位置回到文件的起始位置
-
-
文件读取结束的判定
- 文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
例如:
fgetc 判断是否为 EOF .
fgets 判断返回值是否为 NULL . - 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
例如:
fread判断返回值是否小于实际要读的个数。
- 文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )