C语言核心知识点相关总结(原创) 零:声明、定义、初始化 声明:extern 可以置于变量或者函数前面,提示编译器遇到这个变量或者函数的时候,在其他/当前模块里寻找。 extern int a; // b.c 有个全局变量a,那么可以在a.c里声明这个a,然后拿过来用,但是并不代表我重新定义了一个a extern int function(int a, double b, char c); // 注意:函数在进行声明的时候,extern 是默认可以省略的。 定义:没有加上 extern 就是定义了。定义后的变量是有存储空间的,可以获取到这个变量的地址,但是不一定有值。 int a; int arr[10]; 初始化:定义变量的同时进行赋值操作。 int a = 10; int arr[10] = { 0 }; 数据类型: 在C语言里的数据类型分为四大种: 1. 基本类型 在不同平台下所占字节数: Linux64 Windows 64 Linux32 Windows 32 字符: char 1 1 1 1 整型: short 2 2 2 2 int 4 4 4 4 long 8 4 4 4 long long 8 8 8 8 size_t 8 8 4 4 浮点型: float 4 4 4 4 (实型)double 8 8 8 8 2. 构造类型 数组:存储了n个相同基本类型的数据,举例:sizeof(int) * n 个字节大小 char str1[5] = "haha"; // 只要用" "括起来的就是字符串,只要是字符串最后一位就是'\0' char str2[5] = {'h', 'a', 'h', 'a', 'h'}; str1[2] = 'm'; printf("%s\n", str1); int iarr[5] = {1, 2, 3, 4, 5}; float farr[5] = {1.1, 2.2, 3.14, 4.15,5.001}; 结构体:存储了n个可以不相同基本类型的数据 struct 联合体/共同体:存储n个可以不相同基本类型的数据,但是这种类型里的所有数据共享同一块内存空间,内存大小是最大的那个数据类型大小 union 枚举:存储了n个相同基本类型的数据,但是使用的时候只能取其中一个值,内存大小是 sizeof(int); enum 3. 指针类型 32位占4个字节,64位占8个字节 int *p; //指针类型变量p就是用来存储地址的,定义指针时候的数据类型,代表这个指针指向的内存空间里存储的值的类型 int a = 10; p = &a; *p = 20; printf("%d\n", a); char *str = "haha"; // char *str 和 char str[5] 都是存储字符串,但是str是一个指针,str是一个数组名 printf("%s\n", str); // char str[5] 存储的"haha"是在栈区, char *str 指向的"haha" 是在常量区,str存储的是这个字符串在常量区的首地址。 *(str + 1) = 'm'; //错误!! 因为str里的内容保存在常量区,所以里面的值不可以修改。 4. 空类型 void 变量 :意思是空类型变量,不接受任何数据 void 函数 :意思是该函数没有返回值 void 指针 :意思是可以接收任何其他类型的指针 一、 字符串函数 <string.h> 1 strlen(char *str); 这个函数返回值是一个字符串的有效长度(除去'\0'), 有别于 sizeof() 运算符; 2 strnlen(char *str, intmaxlen); 这个函数是返回 maxlen 长度以内、不含'\0'的字符串的长度。 3 strcat(char *str1, char*str2); 将参数str2 追加到 str1的后面(覆盖str1后面的'\0') 4 strncat(char *str1, char*str2, int maxlen); 将参数str2 追加到 str1的后面,但是只追加str2的前maxlen个字节长度的字符串。 5 strcmp(char *str1, char*str2); 按字符依次比较两个字符串,直到遇到不同的字符为止: 如果str1大于str2,返回正数(Windows下是返回1,Linux下是返回两个不同字符串的ascii码差值) 如果str1小于str2,返回负数(Windows下是返回-1,Linux下是返回两个不同字符串的ascii码差值) 如果两个字符串相等,返回 0 举例: str1 = "abcde"; str2 = "c"; strcmp(str1, str2); 返回值是(Windows下是返回-1,Linux下是返回两个不同字符串的ascii码差值: -2) 6 strncmp(char *str1, char*str2, int maxlen); 和 strcmp()函数返回值相同,但是只比较前 maxlen 个字符; 7 strcpy(char *str1, char*str2); 将参数 str2 的字符串拷贝到参数 str1 里面。(拷贝的字符包括'\0') 8 strncpy(char *str1, char*str2, int maxlen); 将参数 str2 的前 maxlen 个字符拷贝到 str1 里。 9 strchr(char *str, charch); //原来是 int ch,但是函数在调用的时候,会转换成char ch 在str中查找指定字符ch,如果找到的话返回ch在str中的位置,如果没找到,返回 NULL // (void*) 0 10 strstr(char *str1, char*str2); 在str1中查找指定字符串str2,如果找到的话返回str2在str1中的位置,如果没找到,返回 NULL 11 strtok(char *str, chardelim); 分解字符串 str 为一组字符串子串,用delim做为字符串的分隔符。 strtok()函数每次分隔会把分隔符的位置置为 '\0', 同时会破坏原先字符串的完整性; 调用函数前的字符串和调用函数后的字符,已经不一样了。所以我们在做字符串分隔的时候,更推荐使用 sscanf(); 12 sscanf(char *str, char*format, argument...); scanf()函数是从键盘上读取用户输入,然后把值写到变量里; sscanf()函数是从str里读取数据,按照format格式,将数据写入到变量里。 返回值是成功写入的数据数量; int a, b, c; char s1[10] = { 0 }; char s2[10] = { 0 }; char s3[10] = { 0 }; sscanf("2016-05-31", "%d - %d - %d",&a, &b, &c); // 按照 %d - %d - %d 的格式分隔,把2016、05、31 这三个整数写入到a、b、c里 sscanf("2016 - 05 - 31", "%s - %s - %s",s1, s2, s3); // 按照 %s - %s - %s 的格式分隔,把2016、05、31 这三个字符串写入到s1、s2、c里 // %s 匹配一串非空白字符,从输入字符中的第一个非空白字符开始匹配到下一个空白字符之前, // 或者匹配到指定的宽度,赋值参数的类型是char *,末尾自动添加'\0'。 13 sprintf(char *str, char*format, argument...); printf()函数是把格式化后的结果输出到屏幕上; sprintf()函数是把格式化后的结果写入到字符串str里; 返回值是 str被写入的字节数,不包括'\0'; char i[10] = "I"; char you[10] = "You"; char str[100] = { 0 }; sprintf(str, "%s love %s", i, you); // 输出字符串"I love You"到字符串 str 里 sprintf(str, "%10.3f", 0.1234567); // 输出字符串 " 0.123" 到字符串 str 里(原先的内容会被清空) 14 字符串转换函数<stdlib.h> atoi(); 把一个 char 类型的数组转换成一个 int. itoa()把一个 int 类型的数字转换成char类型的字符串(只能在Visual C++ 编译器下使用); // Linux是没有的 atoll(); 把一个 char 类型的数组转换成一个 long long. atof(); 把一个 char 类型的数组转换成一个 double. 举例: char str[10] = "....."; //int float long long int / double / long long n = atoi(str) / atof(str) /atoll(str); printf("%d / %lf / %ld\n", n); 二、函数参数的进栈顺序和运算顺序(引伸出各个平台编译器的不同) 1. 大端对齐和小端对齐: unsigned int num = 0x12345678; 大端对齐:数值的高位字节存储在内存的低位地址上,数值的低位字节存储在内存的高位地址上。 地址: 0xff1100 0xff1101 0xff1102 0xff1103 数值: 0x12 0x34 0x56 0x78 小端对齐:数字的高位字节存储在内存的高位地址上,数值的低位字节存储在内存的低位地址上。 地址: 0xff1100 0xff1101 0xff1102 0xff1103 数值: 0x78 0x56 0x34 0x12 大端:IBM、SUN的服务器CPU都是大端对齐,最早的苹果电脑PowerPC也是大端。 小端:x86\AMD64(美国)架构CPU(复杂指令集)都是小端对齐,ARM(英国)架构CPU(精简指令集)都是小端对齐。 x86 intel AMD64 AMD 2. 函数的进栈顺序 #include <stdio.h> void func(int a, int b,int c) // 三个形参(本质是局部变量),接收实参的值 { printf("a = %d : %p", a, &a); printf("b = %d : %p", b, &b); printf("c = %d : %p", c, &c); } int main(void) { func(100, 200, 300); // 三个实参 return 0; } // Ubuntu GCC 下编译结果 a = 100 : 0xbf8decb0 +4 b = 200 : 0xbf8decb4 +4 c = 300 : 0xbf8decb8 // Windows Visual C++ 下编译结果 a = 100 : 0x0018F720 +4 b = 200 : 0x0018F724 +4 c = 300 : 0x0018F728 // LLVM Clang 下编译结果 a = 100 : 0x7fff547d59e8-4 b = 200 : 0x7fff547d59e4-4 c = 300 : 0x7fff547d59e0 C程序在执行的时候,先入栈的数据是在栈底的,栈底是高地址,后入栈的数据在栈顶,栈顶为低地址。 从上面的例子看得出来: GCC和MSVC下,参数的进栈顺序是"从右往左"。 在LLVM Clang下,参数的进栈顺序是"从左往右"。 3. 函数参数的计算顺序 //1. #include <stdio.h> int main(void) { int a = 10, b = 20, c = 30; printf("%d, %d, %d\n", a + b + c, b = b * 2, c = c* 2); return 0; } // Windows Visual C++ 下编译结果 110, 40, 60 // Ubuntu GCC 下编译结果 110, 40, 60 // LLVM Clang 下编译结果 60, 40, 60 //2. #include <stdio.h> int a() { printf("a\n"); return 1; } int b() { printf("b\n"); return 2; } int main(void) { printf("%d, %d\n", a(), b()); return 0; } //MSVC 下编译结果 b a 1, 2 // Ubuntu GCC 下编译结果 b a 1, 2 // LLVM Clang 下编译结果 a b 1, 2 4. 函数的默认参数 #include <stdio.h> void func(int a, int b,int c = 300) // 三个形参(本质是局部变量),接收实参的值 { printf("a = %d : %p", a, &a); printf("b = %d : %p", b, &b); printf("c = %d : %p", c, &c); } int main(void) { func(100, 200); // 三个实参 return 0; } 上面的写法,在LLVMClang下是可以编译通过的,而且c的值是300,func(100, 200)的值也给了a 和 b。 但是在 MSVC 和 GCC下不允许这么做,也不允许在函数参数列表里赋值。 C编译器: Microsoft Visual C++ / GNUGCC /LLVM Clang / ICC / Turbo C 当一个函数的参数列表里有多个参数的时候,C语言没有规定实参的进栈顺序和计算顺序,而是由编译器自行决定的。 我们在写代码的时候,尽量不要写出UB(行为未定义、奇葩)语句: "UndefinedBehavior"简单来说就是: 如果你的程序违反了C标准中某些规则,程序具体执行结果会发生什么,C语言没有定义。 也就是说得到的结果可能是某种奇怪的情况,都是有可能发生的。 比如说,整数溢出就是一个"UndefinedBehavior"语句。 "UnspecifiedBehavior"简单来说就是: C标准提供了好多种可选方案,但是没有告诉你一定要用哪一种, 比如说,函数参数的计算顺序就是这种情况。 三、一级指针 1. 指针的使用: 32位系统下是 4 个字节,64位系统下是 8 个字节 1)在定义的时候用 * 号,代表这个变量那个是指针类型 int a = 10; //定义一个整型变量,存储整数 10 int *p = &a; //定义一个整型指针变量,存储a的地址 2)在配合表达式使用 * 号,代表取值运算符,可以取出这个地址里的值 printf("%d\n", *p); // 打印p指向的地址里的值 printf("%d\n",*(&a)); // 打印a这个地址里的值 printf("%d\n", *p + 1); // 取出值,再加1打印出来 2. 指针的几种特殊定义方式: 1) int * const p; 指针常量:p 是 int*类型,那么const修饰的是p,所以p是常量,表示p指向的地址不可修改, 也就是说,p不能再指向别的地方了,但是可以修改p指向的这个地址里的值。 举例: int a = 10; int b = 20; int * const p = &a; p = &b; //错误 *p = 100; // 允许 2) const int *p; int const *p; 常量指针:p 是 int*类型,那么const修饰的是*p,所以*p是常量,表示p指向的地址里的值不可修改, 也就是说,p里的值不能再重新赋值了,但是可以修改p指向的地址。 int a = 10; int b = 20; const int *p = &a; p = &b; //可以 *p = 100; // 错误 3) const int * const p; 常量指针常量:p 是 int*类型,那么const分别修饰了p 和 *p,所以p和*p都是常量,表示p指向的地址不可修改, 同时p指向的地址里的值也不可修改。 int a = 10; int b = 20; const int *const p = &a; p = &b; //错误 *p = 100; //错误 《C Primer Plus》 : "自由的代价,是永远的警惕。" 你定义了一个指针,那就一定要知道这个指针指向的什么地方,而且你要保证这个指针是真实有效的,否则我就用程序崩溃来惩罚你。 四、多级指针 #include <stdio.h> int main(void) { int a = 10; int *p = &a; //定义一个一级指针变量,存储了整型变量a的地址 int **pp = &p; //定义一个二级指针变量,存储了整型一级指针变量p的地址 int ***ppp = &pp; //定义了一个三级指针变量,存储了整型二级指针变量pp的地址 printf("%p, %p, %p, %p\n", &a, &p,&pp, &ppp); // 分别打印各个变量自身所在的内存地址 printf("%p, %p, %p, %p\n", &a, p, pp, ppp); //printf("%d", a); 用%d的形式打印a的值:整数 //printf("%p", p); 用%p的形式打印p的值:地址 // &a : 打印变量 a 的地址 // p:打印变量 a的地址 // pp: 打印变量 p 的地址 // PPP:打印变量 pp 的地址 printf("%p, %p, %p, %p\n", &a, p, *pp, **ppp); // &a : 打印变量 a 的地址 // p:打印变量 a 的地址 // *pp:打印变量 a 的地址 //**PPP: 打印变量 a 的地址 printf("%d, %d, %d, %d\n", a, *p, **pp, ***ppp); // a: 打印 10 // *P: 打印 10 // **pp:打印 10 // ***ppp: 打印 10 } 五、指针 和 数组的用法 int num[5] = {10, 20, 30, 40, 50}; int *p = num; 打印的值 打印后*p的值是 数组里的原值 // 操作地址 *p++ 10 20 10 *(p++) *和++的优先级相同,根据结合性(从右往左),那么p先和后自增运算符++结合, ++操作将在表达式完成后进行自增,也就取出p指向的值之后,p指向的下标后移一位(4个字节)。 *++p 20 20 10 *(++p) *和++优先级相同,根据结合性(从右往左),那么p先和前自增运算符++结合, ++操作将会立即完成,p指向的下标后移一位(4个字节),然后再取出p指向的值。 // 操作数值 (*p)++ 10 11 11 根据优先级()小括号优先级最高,p先和*相结合,然后再和后自增运算符++结合, 因为是后自增,所以先打印当前下标的值,然后在原值的基础上自增 1,此时原值已被改变 ++*p 11 11 11 ++(*p) 根据结合性/优先级,*和p先结合,然后再和前自增运算符++结合, 因为是前自增,所以先在原值的基础上自增1,然后在打印这个值,此时原值已被改变。 总结:如果一个表达式里有多个运算符,则先进行优先级比较,先执行优先级高的运算符; 如果优先级相同,那就看结合性,根据结合方向来做运算。 结合性: 从左往右: 小括号()、数组括号[]、成员选择 . 和 ->,双目运算符,逗号运算符 从右往左: 单目运算符、三目运算符、赋值类运算符 /* ++a: 是直接从变量 a 所在的内存地址中取值,并进行加1操作,再执行表达式剩余部分。 a++: 先把变量的值保存在一个临时寄存器里,然后再执行整个表达式,执行完之后,再把a的值自增1,再返回内存里。 CPU -》 寄存器 -》 缓存(L1\L2\L3) -》来自于内存 CPU只和寄存器做数据交换,对于重复操作的数据会放在缓存里。 但是不管寄存器还是缓存,他们的数据都来自于内存。 */ 六、指针数组 和 数组指针 1. 指针数组: 定义形式: int *p[n] = { 0 }; []的优先级高于*,那么p先和[]结合,说明这是一个数组。 再和int *结合,说明这个数组里的每个元素都是一个指针,每个元素都能保存一个地址。 1) 使用指针数组保存多个数据的地址 int main(void) { int a[3] = { 10, 20, 30}; int *p[3] = { 0 }; for (int i = 0; i < 3; ++i) p = &a; for (int i = 0; i < 3; ++i) { printf("%p\n", p); printf("%d\n", *p); } } 2) 使用指针数组保存多个字符串的首地址 #include <stdio.h> int main(void) { char *str[5] = { "ISO/IEC9899:2011", "Programming", "Dennis Ritchie", "c", "bell ssss" }; char *str1 = str[1]; //取出 第2行的字符串 char *str2 = *(str + 3); // 取出 第4行的字符串 char ch1 = *(*(str + 4) + 2); //取出 第5行的字符串的第3个字符 char ch2 = (*str + 5)[7]; //取出第1个字符串的第6个字符,并以此做数组首元素,向后遍历到第7个。 char ch3 = *str[0] + 6; // 取出第1个字符串的第1个字符,然后把这个字符ASCII码加上6,再用%c打印出来 printf("str1 = %s\n", str1); // Programming printf("str2 = %s\n", str2); // c printf("ch1 = %c\n", ch1); //l printf("ch2 = %c\n", ch2); // 2 printf("ch3 = %c\n", ch3); // O (不是0) return 0; } 2. 数组指针(行指针) 定义形式: int *p; int (*p)[n]; int (*p)[n][m]; ()和[]的优先级相同,但是结合性是从左往右,那么p先和*相结合,说明这是一个指针。 然后再和[]结合,说明这个指针指向了一个数组。 示例: 1) #include <stdio.h> void func(int (*p)[3], intn); int main(void) { int a[2][3] = {10, 20, 30, 40, 50, 60}; // 行: 0 1 // 列: 0 1 2 0 1 2 func(a, sizeof(a)); return 0; } void func(int p[2][3], intn); // 明确行和列的数量 void func(int p[][3], intn); //明确列的数量 void func(int (*p)[3], intn) //明确列的数组指针 { printf("%d\n", p[1][2]); // 取出第2行的第3个元素:60 printf("%d\n", **p); //p是行指针,先是取出当前行的列地址,再取出这个列地址里的值: 10 printf("%d\n", (*p + 1)[2]); // 先取出列地址,然后往后移动一个int(4个字节),再以这一列的下标为起点,取出后面第2个元素的值:40 printf("%d\n", *(*p + 1)); // 先取出列地址,往后移动1位(1个int),再取出这个下标的值:20 printf("%d\n", *(p[1] + 2)); // 先取出p[1]的列下标地址,再往后移动2位(2个int),再取出这个下标的值:60 printf("%d\n", *(*(p + 1))); // 先将行指针p后移一位(3个int),再取出这一行的这一列的下标地址,再根据这个下标取值:40 printf("%d\n", *((*p + 1) + 2)); //这个表达式相当于 *(*p + 3),先取出列下标地址,然后往后移动3位(3个int),再取值:40 printf("%d\n", *(*p + 1) + 2); // 先取出列下标的地址,然后往后移动一位(1个int),再进行取值得出20,再加2,结果是:22 } // 三维数组同理 void func(int (*p)[3][4]) { printf("%d\n", *(*(*(p + 0) + 1 ) + 2); //70 } int main(void) { int a[2][3][4] = { {{10, 20, 30, 40}, {50, 60, 70, 80}, {90, 100,110, 120}}, {{11, 22, 33, 44}, {55, 66, 77, 88}, {99, 101,111, 121}} }; func(a); return 0; } 总结: 1.指针数组:就是一个数组,这个数组里的每个元素都是一个指针,这个数组在内存空间中占用了n个指针的大小。 2.数组指针:就是一个指针,这个指针指向一个数组,这个指针在内存空间中占用一个指针的大小。 3.二级指针p 和二维数组名p 的区别: int **p; 这个是一个整型的二级指针,p是一个可变句柄/钥匙,我们可以让这个指针指向任何我们希望它指向的地方。 这个句柄不需要指定内存空间的大小。 int p[n][m]; 这个是一个整型的二维数组,p是引用了这块内存空间的句柄/钥匙,数组在定义的时候, 就被固定指向某个内存空间了,这个空间大小是 sizeof(int) * n * m,而且不可修改p的指向, p就是一个不可变的常量,永远的都只能指向这里了。 如果想确定某个一维数组的值: * 、 [] 如果想确定某个二维数组的值:** 、 [][] 、 *[] 七、内存四区 stack: 栈区,是由编译器自动分配和释放,主要是存放函数参数的值,局部变量的值。 heap:堆区,是由程序员自己申请分配和释放,需要 malloc(); calloc(); realloc();函数来申请,用free()函数来释放 如果不释放,可能出现野指针。 **函数不能返回指向栈区的指针,但是可以返回指向堆区的指针。** data:数据区 -> 静态(全局)区 和 常量区 静态(全局)区:标有 static 关键字,保存了静态变量和全局变量 1. 初始化的全局变量和初始化的静态变量,在一块区域; (data段存放在编译阶段(而非运行时)就能确定的数据,可读可写。 也就是通常所说的静态存储区,赋了初值的全局变量和赋初值的静态变量存放在这个区域,常量也存放在这个区域;) 2. 未初始化的全局变量和为初始化的静态变量,在一块区域; (BSS段通常是指用来存放程序中未初始化的全局变量和静态变量的一块内存区域。特点是可读写的,在程序执行之前BSS段会自动清0) 3. 静态变量的生命周期是整个源程序,而且只能被初始化一次,之后的初始化会被忽略。 (如果不初始化,数值数据将被默认初始化为 0, 字符型数据默认初始化为 NULL)。 常量区:这里的数据是只读的,常量和字符串都保存在这里。(不包括字符数组类型的字符串 -> 栈区) 除了第一次初始化外,常量区的数据在程序执行的时候不允许再次赋值。 整个数据区的数组,在程序结束后由系统统一销毁。 code:代码区,用于存放编译后的可执行代码,二进制码,机器码。 /* BSS段:BSS段(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。 BSS是英文Block Startedby Symbol的简称。BSS段属于静态内存分配。 数据段:数据段(datasegment)通常是指用来存放程序中已初始化的全局变量的一块内存区域。 数据段属于静态内存分配。 代码段:代码段(codesegment/text segment)通常是指用来存放程序执行代码的一块内存区域。 这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为 可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。 堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当 进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用 free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减) 栈(stack):栈又称堆栈, 是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量 (但不包括static声明的变量,static意味着在数据段中存放变量)。除此以外,在函数被调用时, 其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。 由于栈的先进先出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把 堆栈看成一个寄存、交换临时数据的内存区。 */ static 关键字详解: static 在C语言里面既可以修饰变量,也可以修饰函数。 static 变量: 1. 静态局部变量:在函数中定义的,生命周期是整个源程序,但是作用域和自动变量没区别。 都是只能在定义这个变量的函数范围内使用,而且只能在第一次进入这个函数时候被初始化, 之后的初始化会跳过,并保留原来的值。退出这个函数后,尽管这个变量还在,但是已经不能使用了。 2. 静态全局变量:全局变量本身就是静态存储的,但是静态全局变量和非静态全局变量又有区别: 1) 全局变量:变量的作用域是整个源程序,其他源文件也可以使用,生命周期整个源程序。 2) 静态全局变量:变量的作用域范围被限制在当前文件内,其他源文件不可使用,生命周期整个源程序。 static 函数(内部函数): 只能被当前文件内的其他函数调用,不能被其他文件内的函数调用,主要是区别非静态函数(外部函数) 总结: 作用域:变量或函数在运行时候的有效作用范围 。 生命周期:变量或函数在运行时候的没被销毁回收 的存活时间。 作用域 生命周期 局部变量 所在代码块内 所在函数结束 全局变量 所有文件内 程序执行结束 静态局部变量 所在代码块内 程序执行结束 静态全局变量 当前文件内 程序执行结束 普通函数 所有文件内 程序执行结束 静态函数 当前文件内 程序执行结束 八、堆区内存 #include <stdlib.h> 1. void* malloc(n * sizeof(int)); 请求 n 个连续的、每个长度是一个int大小的堆空间,如果创建成功,将返回这个堆空间的首地址,如果创建失败,返回 NULL; 2. void* calloc(n, sizoef(int)); 请求 n 个连续的、每个长度是一个int大小的堆空间,如果创建成功,将返回这个堆空间的首地址,如果创建失败,返回NULL ; (和 malloc() 函数的区别在于,calloc()在创建成功后,会把空间自动初始化为 0 ); 3. void *realloc(p, n * sizeof(int)); 给一个已经分配了地址的指针 p 重新分配空间,p 是原来空间的首地址,n * sizeof(int) 基于这个首地址重新分配的大小; 1) 如果当前内存段后面有足够的内存空间,那么就直接扩展这段内存,realloc()返回原来的首地址; 2) 如果当前内存段后面没有足够的内存空间,那么系统会重新向内存树申请一段合适的空间,并将原来空间里的数据块释放掉, 而且 realloc() 会返回重新申请的堆空间的首地址; 3) 如果创建失败,返回 NULL, 此时原来的指针依然有效; 4. void free(); 1) free(p); 只是释放了申请的内存,系统将这块内存标记为可用。也就是可以被其他进程使用,但是并不改变 p 的指向; 2) p 所指向的内存空间被释放,所以其他程序就有机会使用这段空间了,相当于 p 指向了不属于自己的空间,里面的数据也是未知的。 (这个就叫野指针) 3) 为了避免野指针,最好在 free(p)之后,将 p = NULL; void *(0); 4) free()函数在执行的时候,其实是把这个块内存返回了内存红黑树上,让别人可以使用这块内存。 从逻辑上来说,释放p之后,你是不能再访问原先p指向的这块内存了,但是现在操作系统没有做到, 所以你还是可以访问到这块内存的,只是里面可能存有的数据不属于你。 free(p)之后,其实系统并没有做数据清空处理,所以你既可以访问这个空间,也可以用里面的值。 但是严格意义上来说,这样做是非法的!会造成野指针! 示例: // 如何释放自定义函数内申请的堆空间 #include <stdio.h> #include <stdlib.h> #include <string.h> char *funcA(); char *funcB(); int a = 10; // 全局 初始化区域 char *p1; // 全局 为初始化区域 int main(void) { int b; //栈区 char arr[] = "hello"; // 栈区 char *p2; //p2 在栈区 const char *p3 = "world!"; // p3 在栈区,"world!\0" 在常量区 static int c = 0; //静态区 初始化区域 char *p; p1 = (char *)malloc(20); //p1 指向堆区 20个字节 memset(p1, 0, sizeof(char) * 20); // 使用memset()函数将内存空间初始化为 0 strcpy(p1, "Are you Sleep?"); // "Are you Sleep?" 是在常量区 printf("%s\n", p1); // p1 指向的 拷贝到堆空间的 "Are you Sleep?" 的首地址,通过首地址打印这个字符串 p2 = funcB(); //p2 接收了funcB()回传堆空间首地址,可以通过这个地址找到funcA()申请的堆空间 free(p2); //也可通过 p2 释放自定义函数里申请的对空劲啊 free(p1); p1 = NULL; //安全起见,释放堆空间指针后,重置将指针变量置为 NULL p2 = NULL; return 0; //返回 0 给系统表示 main()正常执行结束,也就代表程序执行结束 } char *funcA() { int a = 10; const char *pa = "1234567"; // pa 在栈区, "1234567\0" 在常量区 char *pb = NULL; // pb 在栈区,pb 占4字节(32bit system) pb = (char *)malloc(20); //pb 指向了一个20个字节大小的堆空间 strcpy(pb, "Yes, I'm!"); // 拷贝字符串给 pb,"Yes, I'm!\0" 在常量区 return pb; //返回 指针变量 pb 保存的堆空间首地址给调用函数funcB() } char *funcB() { char *pa = NULL; //pa 是一个栈上的指针变量 pa = funcA(); //pa 接收了 funcA()函数返回的堆空间地址 return pa; //返回指针变量 pa 保存的堆空间首地址给调用函数 main(); } 论空间分配速度: 栈区确实略快于堆区, 使用栈的时候,是直接从分配的地址里读取值,放到寄存器里,然后再放到目标地址。 使用堆的时候,是先将分配的地址放到寄存器里,然后再从这个地址里取值,再放到寄存器里,再放到目标地址。 论空间访问速度: 栈区和堆区是一样的,都是一个直接寻址的过程,没有区别。 CPU -> 寄存器 > L1 >L2 > L3 (属于缓存) > RAM(内存) > SRAM(主板的存储器) > 硬盘 CPU 只和 寄存器做数据存取,寄存器是用来存储临时数据的,对于需要重复操作的数据,会放到缓存里。 不管是寄存器还是缓存,数据都来自于内存, 内存呢又分为四个区.... 九、文件操作; 数据I/O流 #include <stdio.h> 1 fopen()函数:打开文件 函数原型:FILE *fopen(char restrict *filename, char restrict *mode); // restrict C99标准才引进的,属于类型修饰符,表示修饰的这块内存空间只能被这个指针引用和修改,除此之外别无他法。 参数: filename: 需要打开的文件 mode: 文件打开方式 r 以只读的方式打开文件,前提是这个文件必须存在(只写 r 默认是文本文件) r+ 以可读可写的方式打开文件,前提是这个文件必须存在(默认是文本文件)。 rb 以只读的方式打开一个二进制文件,前提是这个文件必须存在。 rb+ 以可读可写的方式打开一个二进制文件,前提是这个文件必须存在。 w 以只写的方式打开文件,如果这个文件不存在,就创建这个文件;如果这个文件存在,则清空内容。 w+ 以可读可写的方式打开文件,如果这个文件不存在,就创建这个文件;如果这个文件存在,则清空内容。 wb 以只写的方式打开一个二进制文件,如果这个文件不存在,就创建这个文件;如果这个文件存在,则清空内容。 wb+ 以可读可写的方式打开一个二进制文件,如果这个文件不存在,就创建这个文件;如果这个文件存在,则清空内容。 a 以追加的方式打开只写文件,如果这个文件不存在,就创建这个文件;如果这个文件存在,则在文件尾部追加内容。 a+ 以追加的方式打开一个可读可写的文件,如果这个文件不存在,就创建这个文件;如果这个文件存在,则在文件尾部追加内容。 ab 以追加的方式打开一个二进制只写文件,如果这个文件不存在,就创建这个文件;如果这个文件存在,则在文件尾部追加内容。 ab+ 以追加的方式打开一个二进制可读可写文件,如果这个文件不存在,就创建这个文件;如果这个文件存在,则在文件尾部追加内容。 r(read): 读; w(write):写; a(append):追加; +(plus):读或写,主要是配合r、w、a使用; t(text):文本文件; b(binary):二进制文件 返回值:如果文件顺利打开,则返回值是指向这个文件流的文件指针, 如果文件打开失败,返回 NULL (void*)0 一般来说,文件打开失败会做一个文件指针错误判断 FILE *fp = fopen("c:\\code\\text.c","w+"); if(NULL == fp) { //code //exit(-1); } 2 fgetc(); 和 fputc(); 1) fgetc() 文件字符读取函数 原型: int fgetc(FILE * stream); 参数: stream:文件流 返回值:成功返回获取的字符ASCII码,失败返回 EOF(-1); 举例: char ch = fgetc(fp); // 从fp指向的文件流里接收文件流里的第一个字符 2) fputc() 文件字符写入函数 原型: int fputc(int ch, FILE * stream); 参数: ch :就是写入的字符,函数在执行的时候,会自动把 ch ASCII码转换成一个 unsigned char 类型。 stream: 文件流 返回值:成功返回输出的字符,失败返回 EOF(-1); 举例: fputc(ch, fp); //把字符ch写入到 fp 所指向的文件流里。 3 fgets(); 和 fputs(); 1) fgets() 读取文件字符串函数 原型:char *fgets(char *str, int size, FILE* fp); 参数: str : 保存从fp指向的文件流里读取的一行字符串。 size: 从文件流里读取的字符串不超过 size 个字符。( 一般会使用size - 1,留一个字符位置给 '\0') fp :文件指针 返回值:成功返回读取的字符串所在的内存首地址,失败返回 NULL(0); 举例: char str[20] = { 0 }; fgets(str, 20 - 1, fp); // 从fp指向的文件流的第一行里读取 19个字符,然后放到字符数组 str里。 2) fputs() 写入文件字符串函数 原型:int fputs(char *str, FILE* fp); 参数: str: 要写入到文件里字符串(不包括'\0') fp: 文件指针,成功写入一个字符串后,文件指针会自动后移 返回值:成功为写入的字符个数,失败则返回 EOF(-1)。 举例: char str[20] = "Hello Kitty!"; fputs(str, fp); //向 fp 指向的文件流里写入一个字符串 str,具体怎么写,看 mode 属性。 4 fprintf(); 和 fscanf(); 1) fprintf() 将格式化后的数据写入到文件流里 原型: int fprintf(FILE *stream, char *format, argument...); 举例: int i = 10; float f = 3.14; char ch = 'C'; char str[10] = "haha"; fprintf(fp, "%d %f %c %s\n", i, f, ch,str); // 将各个数据按格式写入到文件流里 2) fscanf() 从文件流里获取数据格式化写入输入流里 原型: int fscanf(FILE *stream, char *format, argument... ); 举例: int i; float f; char ch; char str[10]; fscanf(fp, "%d,%f", &i, &f); // 如果不需要从文件里面写入字符串,那么就可以用逗号或者其他符号来分隔 fscanf(fp, "%s%c", str, &ch); // 如果文件里需要写入字符串,那么字符串与其他数据之间只能用空格和回车来分隔 5 fread(); 和 fwrite(); 二进制文件读写函数 函数原型: size_t fread(void *ptr, size_t size,size_t count, FILE* fp); size_t fwrite(void *ptr, size_t size,size_t count, FILE* fp); 参数: ptr: 是一个指针,对应 fread()来说,是从文件里读入的数据存放的地址; 对应 fwrite()来说,是写入到文件里的数据存放的地址。 size: 每次要读写的字节数 count : 读写的次数 fp: 文件指针 返回值:成功读取/写入的字节数 举例: char str[] = { 0 }; fread(str, sizeof(char) * 10, 1,fp); // 每次从fp指向的文件中读取10个字节大小,放入字符数组 str中,总共读1次 fwrite(str, sizeof(char) * 10,1, fp); // 每次从str里获取 10个字节大小,写入到 fp 指向的文件中,总共写1次 6 fseek(); 文件指针操作函数 函数原型: size_t fseek(FILE* fp, long offset, int whence); 参数: fp : 文件指针 offset: 偏移量,基于起始点偏移了 offset 个字节 whence : 起始点(三个): SEEK_SET 0 文件开头位置 SEEK_CUR 1 当前位置 SEEK_END 2 文件结尾位置 举例 fseek(fp, 0, SEEK_END); //将文件指针指向文件结尾,并偏移了 0 个字节,也就是直接将文件指针指向文件结尾 fseek(fp, -10, SEEK_CUR); //将文件指针指向当前位置,并偏移了 -10 个字节,也就是将文件指针往前移动10个字节 7 ftell(); 文件指针操作函数 函数原型: long ftell(FILE* fp); 参数: fp 文件指针 返回值:返回文件指针当前位置,基于文件开头的偏移字节数, 举例: long len = ftell(fp); // 返回文件指针当前位置,基于文件开头的偏移字节数,保存到 len 里。 8 rewind(); 文件指针操作函数 函数原型: void rewind(FILE* stream); 参数: fp 文件指针 举例: rewind(fp); // 将文件指针重新指向I/O流(文件流)的开头。 stream > istream / ostream -> fstream -> sstream 6\7\8 大例子 FILE *fp = fopen("C:\\code\\a.txt","r+"); fseek(fp, 0, SEEK_END); //将文件指针指向文件结尾 long len = ftell(fp); //获取文件指针位置,得到文件的大小(Byte) rewind(fp); //将文件指针重新指向文件开头 9 fflush(); 清空数据流里的数据 函数原型: void fflush(FILE* stream); 参数: stream 数据流 举例: fflush(fp); // 清空文件流 fflush(stdin); // 清空输入流 fflush(stdout); // 清空输出流 10 int stat(const char*path, struct stat *buf); // 自行补充 11 rename(); 和 remove(); rename(FILE* filename1, FILE* filename2); rename("old_name.txt","new_name.txt"); // 把old_name.txt 重命名为 new_name.txt remove(FILE* filename); remove("C:\\code\\a.txt"); // 将绝对路径下的 a.txt 文件删除 12 feof(); 原型: int feof(FILE* fp); 参数: fp 文件指针 返回值:一旦文件指针指向文件结尾,就返回一个真值;否则返回非真值(0) 1. 这个函数达到文件结尾的时候,返回的是一个真值,所以在做判断的时候要注意 !feof(fp) 2. 这个函数必须对文件进行过一次读写操作才会生效,也就是说哪怕这个文件是空的,也必须读写一次,feof()才会返回真值。 文件结束是一个标识符,每次对文件读写都会修改这个标识符的位置,对文件读写一次,文件标识符才会被找到,feof()做出返回操作。 13 fclose(); 原型: int flcose(FILE* fp); 参数: fp 文件指针 返回值:如果成功释放,返回 0,否则返回 EOF(-1); fclose(fp); 表示释放文件指针和相关的文件缓冲区,文件指针不再合法指向那块区域,但是不代表清空对应的区域。 UTF-8 编码格式下 一个汉字 3个字节 GBK 编码格式下 一个汉字 2个字节 // UTF-8 下 汉字逆置原理 #include <stdio.h> #include <string.h> int main(void) { char str[] = "阿基米德"; int len = strlen(str); // printf("%c%c\n", str[0], str[1]); // 打印"阿" for (int i = len - 1; i > 0; i -= 3) // UTF-8 编码下是3个字节,GBK下是2个字节 { printf("%c%c\n", str[i - 1], str); } return 0; } 不同操作系统的行尾标志: CR LF \ CR \ LF CR 是 '\r' 回车 LF 是 '\n' 换行 在DOS和NT内核的Windows下,采用的是 回车+换行(CR LF '\r''\n') 来表示下一行的开始 在Unix/Linux下,采用的是 换行(LF '\n') 来表示下一行的开始 在Macintosh下(OS X) ,采用的是 回车(CR '\r') 来表示下一行的开始 十、结构体 1. 结构体的字节对齐: 在C语言里,结构体所占的内存是连续的,但是各个成员之间的地址不一定是连续的。所以就出现了"字节对齐". 结构体变量的大小,一定是其最大的数据类型的大小的整数倍,如果某个数据类型大小不够,就填充字节。 结构体变量的地址,一定和其第一个成员的地址是相同的。 1) 结构体字节对齐 #include <stdio.h> #include <string.h> struct Box { // 首先检查结构体成员里最大的数据类型是 double, 占8个字节,则 int height; // 系统判断 int 可以和相邻的 char name[10] 共同填充字节 char name[10]; // char name[10] 需要填充 到double 的倍数,但是可以和相邻的 int 一起累加,再填充4个字节,对齐 double 的2倍 double width; // double 是所有成员里最大数据类型,满足double 的1倍 char type; // char 会填充7个字节,对齐double 的1倍 }; int main(void) { struct Box box; box.height = 4; //高度 strcpy(box.name, "Dropbox"); // 名称 box.width = 5.5; //宽度 box.type = 'C'; //类型 printf("box = %p\n", &box); printf("box.height = %p\n", &box.height); printf("box.name = %p\n", box.name); printf("box.width = %p\n", &box.width); printf("box.type = %p\n", &box.type); printf("box = %d\n", sizeof(box)); // 16 + 8 + 8 = 24 return 0; } 2) 初识链表 #include <stdio.h> #include <string.h> #include <stdlib.h> struct Student { char *name; //姓名 int age; //年龄 struct Student *next; // next 是结构体成员,但是类型是 struct Student * 类型,用来指向某个 struct Student 的结构体变量的。 // 结构体可以看做是一个自定义的数据类型,而且结构体可以嵌套,但是嵌套有条件: // 结构体只可以嵌套自身类型的结构体指针,但是绝对不能嵌套自身类型的结构体变量 // 比如,不能嵌套 struct Student next; 这种 }; int main(void) { struct Student stu, *stup; //定义了一个结构体变量 stu 和一个结构体指针变量 stup stu.name = (char *)malloc(10 * sizeof(char)); // 给姓名申请了一个10个字节的堆空间 strcpy(stu.name, "damao"); // 拷贝字符串 "damao" 给 stu.name (注意,不能直接赋值,要用拷贝) stu.age = 18; //今年 18岁了 stup = (struct Student *)malloc(1 * sizeof(struct Student)); // 给 stup 申请一个堆空间,用来保存两个指针(name,next)和一个int stup->name = (char *)malloc(10 * sizeof(char)); // 给 stup->name 申请一个堆空间,保存字符串 strcpy(stup->name, "ermao"); // 拷贝字符串 stup->age = 16; //今年 16岁了 stu.next = stup; //stu的成员next 指向了结构体变量 stup 的首地址,链表诞生 stup->next = NULL; //stup的成员 next 指向 NULL,保证安全。 free(stup->name); //最后申请的堆 最先释放 free(stup); //继续释放 free(stu.name); //最先申请的堆 最后释放 return 0; // 程序正常结束 } End...
|