C 学习记录

196 阅读10分钟

数据类型

image.png

  • 结构体代码
// 声明1个结构体
typedef struct {
    int a;
    int b;
} Person;

int main() {
    Person *person = malloc(sizeof(Person));
    person->a = 11;
    person->b = 22;
    printf("%d %d \n", person->a, person->b);
    return 0;
}
  • sizeof 使用
void TestArray(int arr[]) {
    printf("TestArray arr size:%llu\n", sizeof(arr));//TestArray arr size:8
}

void test1() {
    int arr[] = {10, 20, 30, 40, 50};
    printf("array size: %llu\n", sizeof(arr));//array size: 20

    //数组名在某些情况下等价于指针
    int *pArr = arr;
    printf("arr[2]:%d\n", pArr[2]);//arr[2]:30
    printf("array size: %llu\n", sizeof(pArr));//array size: 8

    //数组做函数函数参数,将退化为指针,在函数内部不再返回数组大小
    TestArray(arr);
}
  • 修改变量的方式
void test2() {
    int a = 10;
    a = 20;
    printf("直接修改 a:%d\n", a);

    int *p = &a;
    *p = 30;
    printf("间接修改,a:%p %p \n", p ,&a);
    printf("间接修改,a:%d\n", a);
}
  • 位运算
    // ~    按位取反
    // 将每个1变为0,将每个0变为1
    unsigned char a = 2;   //00000010
    unsigned char b = ~a;  //11111101
    printf("ret = %d\n", a); //ret = 2
    printf("ret = %d\n", b); //ret = 253

//     位与 &
//    只有两个操作数的对应位都是1时结果才为1
    int c = 10010011;
    int d = 00111101;
    int e = c & d;
    printf("e = %d \n", e);
//    C也有一个组合的位与-赋值运算符:&=。下面两个将产生相同的结果:
//    val &= 0377
//    val = val & 0377

// 位或 |
// 对于每个位,如果操作数中的对应位有一个是1(但不是都是1),那么结果是1.如果都是0或者都是1,则结果位0
//    (10010011)
//    ^ (00111101)
//    = (10101110)

1 指针

1 指针的定义

指针就是变量,内容就是地址,32位机器指针4个字节,64位机器8个字节
int a = 10;
int *p = &a;
//  int *  指针类型(**指针类型决定了指针进行解引用操作的时候,能访问空间的大小**)
//  p      指针变量
//  &a     取地址
int * : *p可以访问4个字节。
char * : *p可以访问1个字节。
double *  :  *p可以访问8个字节。

2 指针加减

指针加一不是指向下一个紧挨着的地址,是指向下一个指针变量对应的类型变量开始的地址

意义 指针类型决定了:指针走一步走多远(指针的步长)

- 普通类型的指针步长计算
int a = 0;
int *p = &a;
printf("p:%p\n", p);//p:000000000061FDE4
p++;//步长为4个字节
printf("p++:%p\n", p);//p++:000000000061FDE8

-   多级指针的步长计算
int 

3 野指针

野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

  • 1 指针未初始化
  • 2 指针越界访问
  • 3 指针指向的空间已经释放

避免野指针

  • 1 指针初始化
  • 2 小心指针越界
  • 3 指针指向内存释放 即 指向NULL
  • 4 指针只用之前检查有效性

4 指针和数组

void test() {
    int c[9] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
    printf("%p\n", c); // 000000000061FDC0
    printf("%p\n", &c);// 000000000061FDC0
    printf("%p\n", &(c[0]));// 000000000061FDC0
    // 数组是特殊指针,上面3个地址一样
    int *p = c;
    for (int i = 0; i < 9; i++) {
        printf("%d \n", *(p + i));
        printf("%d \n", c[i]);
        printf("%d \n", *(c + i));
    }
}

void testArray() {
    int a[] = {1, 2, 3, 4, 5, 6};
    int *p, *q, *r;
    //
    p = &a[3];
    printf("*p = %d \n", *p);
    //
    q = a;
    q = q + 3;
    printf("*q = %d \n", *q);
    //
    r = a;
    printf("*(r+3) = %d \n", *(r + 3));
    // 以上3种方式结果一样
}


void test() {
    int a = 11;
    int b = 22;
    int c = 33;
    int *p1 = &a;
    int *p2 = &b;
    int *p3 = &c;

    printf("%p %p %p \n", &a, &b, &c); // 000000000061FDD4 000000000061FDD0 000000000061FDCC
    printf("%p %p %p \n", p1, p2, p3); // 000000000061FDD4 000000000061FDD0 000000000061FDCC

    int *arr[3] = {p1, p2, p3};
    printf("%d \n", *arr[0]); //11 取第一个元素对应的值
    printf("%d \n", *arr); // 取aar指针对应的地址
    printf("%d \n", **arr); //11取aar指针对应的地址的值

}
  • 指针数组

指针数组就是存放指针的数组,实际上还是数组,里面存放着不同类型的指针

void test() {
    char *arr[] = {"abcdef", "ghi", "jklmn"};
    int len = sizeof(arr) / sizeof(arr[0]);
    int i = 0;
    for (i = 0; i < len; i++) {
        printf("%s\n", arr[i]);
    }
    printf("%d %d %d \n", sizeof(arr), sizeof(arr[0]), len);
}
  • 数组指针

数组指针—就是指针,该指针指向一个数组

5 指针步长

int main() {
    int a = 0;
    int *p = &a;// 1 指针自身类型 int *  2 指针指向类型 int(决定步长) 3指针指向的内容
    *p = 20;
    printf("%d   %p  %p \n", *p, p, &a);//p2=20   000000000061FE14  000000000061FE14

    printf("p=%p \n", p);//000000000061FE14
    p++;
    printf("p=%p \n", p);//000000000061FE18 指针指向的内容是int,所以步长+4

    int **temp = &p;
    printf("temp=%p %llu \n", temp, sizeof(int *)); // temp=000000000061FE08
    temp++;
    printf("temp=%p \n", temp);//000000000061FE10 指针指向的内容是int *(指针类型 8个字节)(16 进制)


    int *arr[4] = {1, 2, 3, 4};//指针自身的类型 int *[4] 指针指向的类型 int [4]
    printf("temp=%p %llu \n", arr, sizeof(int *)); // temp=000000000061FDE0
    int *arr1 = (int *) arr;
    arr1++;
    printf("temp=%p \n", arr1); // temp=000000000061FDE4

    char charp[5] = {'a', 'b', 'c', 'd', 'f'};
    char (*p1)[4] = &charp;// 指针自身的类型 char (*)[5]  指针指向的内容是 char ()[]
    printf("p1=%p \n", p1);//000000000061FDCB
    p1++;
    printf("p1=%p \n", p1);//000000000061FDCF

    return 0;
}

6 内存分布

在程序没有加载到内存前,可执行程序内部已经分好3段信息,分别为代码区(text)、数据区(data)和未初始化数据区(bss)3 个部分(有些人直接把data和bss合起来叫做静态区或全局区)。

变量 : 全局变量,局部变量,静态变量

  1. 代码区
程序被操作系统加载到内存的时候,所有的可执行代码(程序代码指令、常量字符串等)都加载到
代码区,这块内存在程序运行期间是不变的。代码区是平行的,里面装的就是一堆指令,在程序运行期
间是不能改变的。函数也是代码的一部分,故函数都被放在代码区,包括main函数。
  1. 静态区
静态区存放程序中所有的全局变量和静态变量
  1. 栈区
栈(stack)是一种先进后出的内存结构,所有的自动变量、函数形参都存储在栈中,这个动作由编
译器自动完成,我们写程序时不需要考虑。栈区在程序运行期间是可以随时修改的。当一个自动变量超
出其作用域时,自动从栈中弹出。

每个线程都有自己专属的栈;
栈的最大尺寸固定,超出则引起栈溢出;
变量离开作用域后栈上的内存会自动释放。
  1. 堆区
堆(heap)和栈一样,也是一种在程序运行过程中可以随时修改的内存区域,但没有栈那样先进后出的
顺序。更重要的是堆是一个大容器,它的容量要远远大于栈,这可以解决内存溢出困难。一般比较复杂
的数据类型都是放在堆中。但是在C语言中,堆内存空间的申请和释放需要手动通过代码来完成

image.png

内存管理 一般遵循以下三个原则

1. 如果明确知道数据占用多少内存,那么数据量较小时用栈,较大时用堆;
2. 如果不知道数据量大小(可能需要占用较大内存),最好用堆(因为这样保险些);
3. 如果需要动态创建数组,则用堆。
  • 动态内存是指在堆上分配的内存
  • 静态内存是指在栈上分配的内存

7 二级指针

int twoPoint() {
    int va = 200;
    printf("%d %p\n", va, &va); // 200 000000000061FDEC

    int *p = &va;
    printf("%p  %p  %d\n", p, &p, *p);
    // 000000000061FDEC  000000000061FDE0  200
    // p   是va的地址
    // &p  是指针的地址
    // *p  指针地址对应的值

    int **q = &p;
    printf("%p  %p  %p  %d \n", q, &q, *q, (**q));
    //000000000061FDE0  000000000061FDD8  000000000061FDEC  200
    // q    是二级指针q对应的值,既指针P的地址(&p)
    // &q   是二级指针q的地址
    // *q   q指针(因为是地址)的地址对应值,
    // **q  对(*q)的取值
}

image.png

8 指针常量和常量指针

指针常量:指针本身是常量,也即指针存储的地址是常量,不能改变。但是地址对应的内容是可以改变的 常量指针:指向常量的指针,指针地址对应的值是常量,不能被修改,但是指针可以修改

  • *const (指针常量,指针在前)
  • const * (常量指针,指针在后)
void test() {
    // 指针常量
    int a = 10;
    int *const b = &a;
    //b = 0x111;  指针不能被修改
    *b = 11;// 指针地址对应的值可以被修改
    printf("a=%d\n", a);

    // 常量指针
    int c = 110;
    int d = 120;
    int const *p = &c;// const int *p;
    p = &d;// 指针可以被修改
    // *p = 111;// 值不能被修改

    int q = 1;//变量
    int const w = 12;// 定义常量
    int const *ptr1 = &q;// 常量指针
    int *const prt2 = &w;// 指针常量

    // 错误,不能把常量的地址赋给指针变量
    // int *ptr3 = *w;
    // 正确,可以把常量的地址赋给常量指针
    int const *ptr4 = &w;

    //*ptr1 = 3;   //错误,间接引用常量指针不可以修改内存中的数据
    //*prt2 = 4;   //正确,间接引用指针常量可以修改内存中的数据

    //ptr1 = &w; //正确,常量指针可以指向其他变量
    //prt2 =&w;    //错误,指针常量不可以指向其他变量
    const int *const ptr5 = &a;//常量指针常量
    // *ptr5=5;//错误,不可以间接引用修改内存数据
    // ptr5=&b; //错误,不可以修改指向的对象

}

8 数组指针

int main() {
    int a[3][4] = {{0, 1, 2,  3},
                   {4, 5, 6,  7},
                   {8, 9, 10, 11}};
    // 定义数组指针
    int (*p)[4] = a;
    // 对指针进行加法(减法)运算时,它前进(后退)的步长与它指向的数据类型有关,p 指向的数据类型是int [4],
    // 那么p+1就前进 4×4 = 16 个字节,p-1就后退 16 个字节,这正好是数组 a 所包含的每个一维数组的长度。也就是说,
    // p+1会使得指针指向二维数组的下一行,p-1会使得指针指向数组的上一行。

    // p 指向 {0, 1, 2,  3} 首地址,*p 取首地址, *p+2  取第3个元素
    printf("%p  %p  %d \n", p, *p, *(*p + 2));

    // *(p+1) 取{4, 5, 6,  7}  首地址
    printf("%p   %d \n", *(p + 1), *(*(p + 1) + 1));

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

    return 0;
}
int *(p1[5]); //指针数组,可以去掉括号直接写作 int *p1[5];
int (*p2)[5]; //二维数组指针,不能去掉括号

指针数组和二维数组指针有着本质上的区别: 指针数组是一个数组,只是每个元素保存的都是指针,以p1为例,在32位环境下它占用4×5= 20个字节的内存。 二维数组指针是一个指针,它指向一个二维数组,以上面的 p2 为例,它占用 4 个字节的内存。

9 函数指针

int Max(int, int);

int main() {
    int (*p)(int, int);// 定义一个函数指针
    int a, b, c;
    p = Max;
    //把函数Max赋给指针变量p, 使p指向Max函数
    printf("please enter a and b:");
    scanf("%d%d", &a, &b);
    c = (*p)(a, b); //通过函数指针调用Max函数
    printf("a = %d\nb = %d\nmax = %d\n", a, b, c);
    return 0;
}

int Max(int a, int b) {
    return (a > b) ? a : b;
}