c语言核心总结

xiaoxiao2021-07-06  424

零:声明、定义、初始化

    声明: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. 基本类型                                 Linux 64        Windows 64        Linux 32        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存储的是这个字符串在常量区的首地址。

    4. 空类型         void 变量 :意思是空类型变量,不接受任何数据         void 函数 :意思是没有返回值         void 指针 :意思是可以接收任何其他类型的指针

一、 字符串函数  <string.h> 1 strlen(char *str);      这个函数返回值是一个字符串的有效长度(除去'\0'),  有别于 sizeof() 运算符;

2 strnlen(char *str, int maxlen);     这个函数是返回 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, char ch); //原来是 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, char delim);     分解字符串 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、s3里

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, ".3f", 0.1234567);     // 输出字符串 "     0.123" 到字符串 str 里(原先的内容会被清空)   14 字符串转换函数 <stdlib.h>      atoi();         把一个 char 类型的数组转换成一个 int.         itoa()把一个 int 类型的数字转换成char类型的字符串(只能在Visual C++ 编译器下使用);         // Linux是没有的     atoll();          把一个 char 类型的数组转换成一个 long long.

    atof();         把一个 char 类型的数组转换成一个 float.

    举例:     char str[10] = ".....";  //int float long long     int / float / 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 : 0x7fff547d59ec -4 b = 200 : 0x7fff547d59e8 -4 c = 300 : 0x7fff547d59e4

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; }

上面的写法,在LLVM Clang下是可以编译通过的,而且c的值是300,func(100, 200)的值也给了a 和 b。 但是在 MSVC 和 GCC下不允许这么做,也不允许在函数参数列表里赋值。

C编译器: Microsoft Visual C++ / GNU GCC /LLVM Clang / ICC / Turbo C 

当一个函数的参数列表里有多个参数的时候,C语言没有规定实参的进栈顺序和计算顺序,而是由编译器自行决定的。

我们在写代码的时候,尽量不要写出UB(行为未定义、奇葩)语句: 

"Undefined Behavior"简单来说就是: 如果你的程序违反了C标准中某些规则,程序具体执行结果会发生什么,C语言没有定义。 也就是说得到的结果可能是某种奇怪的情况,都是有可能发生的。 比如说,整数溢出就是一个"Undefined Behavior"语句。

"Unspecified Behavior"简单来说就是: 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[i] = &a[i];

    for (int i = 0; i < 3; ++i)     {         printf("%p\n", p[i]);         printf("%d\n", *p[i]);     } }

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], int n); 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], int n); // 明确行和列的数量

void func(int p[][3], int n); //明确列的数量

void func(int (*p)[3], int n)  //明确列的数组指针 {     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. 初始化的全局变量和初始化的静态变量,在一块区域;             2. 未初始化的全局变量和未初始化的静态变量,在一块区域;             3. 静态变量的生命周期是整个源程序,而且只能被初始化一次,之后的初始化会被忽略。                 (如果不初始化,数值数据将被默认初始化为 0, 字符型数据默认初始化为 NULL )。

        常量区:这里的数据是只读的,常量和字符串都保存在这里。(不包括字符数组类型的字符串 -> 栈区)             除了第一次初始化外,常量区的数据在程序执行的时候不允许再次赋值。

        整个数据区的数组,在程序结束后由系统统一销毁。

code:代码区,用于存放编译后的可执行代码,二进制码,机器码。

    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(内存) > ROM(主板的存储器) >  硬盘 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     : 文件指针     返回值:成功返回读取的字符串所在的内存首地址,失败返回 EOF(-1);     举例:         char str[20] = { 0 };         fgets(str, 20 - 1, fp);        // 从fp指向的文件流的第一行里读取 19个字符,然后放到字符数组 str里。

    2) fputs() 写入文件字符串函数     原型:fputs(char *str, FILE* fp);     参数:    str: 要写入到文件里字符串             fp: 文件指针     举例:         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 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 文件删除

11 feof();     原型: int feof(FILE* fp);     参数:    fp  文件指针      返回值: 一旦文件指针指向文件结尾,就返回一个真值;否则返回非真值(0)

    1. 这个函数达到文件结尾的时候,返回的是一个真值,所以在做判断的时候要注意 !feof(fp)     2. 这个函数必须对文件进行过一次读写操作才会生效,也就是说哪怕这个文件是空的,也必须读写一次,feof()才会返回真值。     文件结束是一个标识符,每次对文件读写都会修改这个标识符的位置,对文件读写一次,文件标识符才会被找到,feof()做出返回操作。

12 fclose();     原型: int flcose(FILE* fp);     参数:    fp 文件指针      返回值: 如果成功释放,返回 0, 否则返回 EOF(-1);

    fclose(fp); 表示释放文件指针和相关的文件缓冲区,文件指针不再合法指向那块区域,但是不代表清空对应的区域。     

UTF-8 编码格式下 一个汉字    3 个字节 GBK 编码格式下    一个汉字     2 个字节 

// UTF-8 下 汉字逆置原理 #include <stdio.h> #include <stdlib.h> #include <string.h>

int main(void) {     char str[] = "基尔加丹";     int len = strlen(str);

    for (int i = len - 1; i > 0; i -= 3)    // UTF-8 编码下是3个字节,GBK下是2个字节     {         printf("%c%c\n", str[i - 1], str[i]);     } //    printf("%c%c\n", str[0], str[1]);    // 打印"基"

    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 {     int height;        // 高     char a[10];     double width;    // 宽     char type;        // 类型 };

int main(void) {     struct Box box;     box.type = 'C';        // C 类型     strcpy(box.a, "hahaha");     box.height = 4;     // 高度是 4     box.width = 5.5;    // 宽度是 5.5

    printf("box = %p\n", &box);     printf("box.height = %p\n", &box.height);     printf("box.a = %p\n", box.a);     printf("box.width = %p\n", &box.width);     printf("box.type = %p\n", &box.type);

    printf("box = %d\n", sizeof(box));     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...

转载请注明原文地址: https://www.6miu.com/read-4821467.html

最新回复(0)