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. 基本类型
                                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, "%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 类型的数组转换成一个 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...

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值