【C3】一些知识点



2021年12月27日    Author:Guofei

文章归类: C    文章编号: 10003

版权声明:本文作者是郭飞。转载随意,但需要标明原文链接,并通知本人
原文链接:https://www.guofei.site/2021/12/27/c3.html


关于指针

二级指针避免内存泄露

错误的代码

void tst(char *i) {
    i = malloc(100); // 这个 i 运行完毕后被清空了
}

int main() {
    char *p = NULL;
    tst(p);
    strcpy(p, "hello"); // 这个 p 仍然为0
    free(p); // 释放不是 malloc 分配的内存
    return 0;
}

正确的代码:用二级指针

void tst(char **i) {
    *i = malloc(100);
}

int main() {
    char *p = NULL;
    tst(&p);
    strcpy(p, "hello");
    free(p);
    return 0;
}

零碎知识

char a = '\72'; // 表示 8 进制的ascii码
char s[5] = {"abc"};  // 也是合法的
int *a, b; // a 是个指针,b是int类型

int (*p)[5]; // 指针指向一个长度为5的整形数组
int *p[5]; // 定义一个长度为5的数组,数组的元素都是指针

函数的传参是如何进行的?

  • int sum(int x,int y);,调用时,x和y以堆栈的形式复制了一份,当函数运行完毕后,复制的这一份数据自动释放。

如何防止频繁申请内存?

  • 如果程序中有很多次 malloc(8)。那么可以事先 malloc(1024*16),然后立即释放掉。这样可以避免频繁向系统要内存。

进阶

数组名不是指针

int arr[] = {1, 2, 3, 4, 5}; // arr 是数组名
int *p = (int *) &arr; // p 是指针

printf("%ld != %ld\n", sizeof(arr), sizeof(p));
//   1.  一个是数组的大小,为4*5;一个是指针的大小,为8

// 2. +1后的指向不同
printf("%p,%p\n", p, p + 1);// +1移动一个int
printf("%p,%p", &arr, &arr + 1); // +1移动5*int

//    3. 数组名是指针常量,不可更改
//    arr = NULL; //错的

指针作为入参

int func(int arr[]){ // 等价于 int *arr

}

typedef

指针的一些梳理

  1. 指针常量、指向常量的指针,已经梳理好

零碎

int a[10]
// ?实际上是定义了一个指针?指向数组的开头

取数 a[3]
实际上是  *(a+3)

指针



// 方法1
char a[10] = "hello";
// 上面等价于:
char a[10] = {'h', 'e', 'l', 'l', 'o', 0};
// 上面是初始化一个字符数组
char *b = "hello";
// 上面是初始化一个字符串常量

// 方法2
char a1[10];
char *a2;

// 1. sizeof(a) 结果不一样
// 2. 初始化方式不一样
// a1 = "hello" 是错的
a2 = "hello"; // 直接就可以赋值,

// 必须用这个:
strcpy(a1, "world");
// 但你不能对指针 a2 用 strcpy,memset 之类的,但是:
// 1. 第二个变量可以是指针,表示copy的是指针指向的内容
// 2. 如果用 malloc 分配内存,就可以给指针赋值:
char *a2;
p = malloc(10);
strcpy(p, "hello");
free(p)
// 下面这个函数,多次调用同一个,也不会有错误
// 因为1)传入的参数都是拷贝,这里是指针的拷贝,因此对指针做自增只作用于函数内。但它指向的内存却可以去修改
// 2)也没有对指针指向的内存做改动
int strlen(char *string) {
    int length = 0;
    while (*string++ != 0) {
        length += 1;
    }
    return length;
}

char *s = “hello” 定义之后,不能改动元素,但 int */char a[] 都是可以改的。
是不是这样解释: char *s = “hello” 是指向的字符串常量,因此不能改动

char *s = "hello";
s[1] = 'c'; // 报错


指针和向量的区别

// 这两个不一样
int a[5]; // 内存中创造一个空间,然后创建一个数组名,它的值是一个常量,指向这段空间
int *b; // 仅仅是创建一个指针本身的内存空间,并没有分配 int 内存空间

// *a 合法,但 *b 不合法,因为访问了一个不存在的位置。

指针加法

int arr1[10];

printf("%p = %p= %p\n", arr1, &arr1, &arr1[0]);
printf("%p = %p\n", arr1 + 1, &arr1[1]); // 指向“下一个元素”
printf("%p = %p\n", &arr1 + 1, &arr1[10]); // 指向“下一个模块”


// 对于二维数组,也是一样的
int mat[3][3] = {
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9}
};


printf("%p = %p\n", mat, &mat[0][0]); // 指向第0行第0个元素: &arr[0][0]
printf("%p = %p\n", mat + 1, mat[1]); // 指向第1行第0个元素: &arr[1][0]
printf("%p\n", &mat + 1); //指向下一块:&arr[2][2] + 1

// 如何用指针指向一个二维数组
int(*p)[3] = mat; // 指向整个数组的指针
printf("%d", *(*(p + 1) + 2));

问题:

  • ` ((arr + 1) + 2) 是什么?是 arr[1][2]`
  • 这是因为 *(arr + 1) + 2
  • arr[1, 2]不会报错,它实际上是 arr[2],导致结果不符合预期

关于溢出

char 在运算中以 int 运算的,例如:

unsigned char a, b;
b = 200;
a = (b * 5) / 5;  // 结果是 200,不会因为超过 255 而溢出,因为它们是以 int 运算的

但是,这个会因为溢出而结果错误

unsigned int a = 0xFFFFFFF0;
long b = (a * 2) / 2;
// 要改成这样才能保证结果正确:
long b = ((long) a * 2) / 2;

一些正确,但不应该写的代码

int a[10]={0,1,2};
int c = 2[a];
// 它相当于 *(2+a),也就相当于 *(a+2),也就相当于 a[2];

实现“子类复用父类的方法”

#include <stdlib.h>
#include <stdio.h>

// 基类结构体
typedef struct {
    int age;
} Animal;

Animal *Animal_New(int age) {
    Animal *animal = malloc(sizeof(Animal));
    animal->age = age;
    return animal;
}


// 基类方法,下面的代码中,它将会被子类复用
void Animal_say(Animal *animal) {
    printf("I'm an animal. Age = %d", animal->age);
}


// 派生类结构体
typedef struct {
    Animal *super;
    char *name;
} Dog;


// 派生类初始化方法
Dog *Dog_New(char *name, int age) {
    Dog *dog = malloc(sizeof(Dog));
    dog->super = Animal_New(age);
    dog->name = name;
    return dog;
}

//实现"复用父类的方法"
void Dog_say(Dog *dog) {
    Animal_say(dog->super);
}

int main() {
    Dog *dog = Dog_New("Tom", 2);
    Dog_say(dog);
    return 0;
}

实现“泛型”

void *data 来定义输入值,函数中使用 sizeof(data) 来确定数据类型,并且在if中实现各自的算法

void algo(void* data, int size) {
    if (size == sizeof(int)) {
        int* p = (int*)data;
        // int类型算法操作
    } else if (size == sizeof(float)) {
        float* p = (float*)data;
        // float类型算法操作
    }
}

还有一些其它方案

  • 用宏定义,例如 #define ADD(x, y) ((x) + (y)) 可以对 int、float 都有用
  • 用 union

项目的代码规范

C 语言的特性要求所有的函数、结构体名字都不能一样,否则编译时会重复,这导致命名会很长、很丑,可以这样

#ifndef MYPROJECT_MY_LIB_H
#define MYPROJECT_MY_LIB_H

#define T My_Project_Stack_T;

//一大堆函数,大量用到 T
//如此,在这个文件中,可以用 T 来表示 My_Project_Stack_T,大大减少了阅读负担

#undef T
#endif //MYPROJECT_MY_LIB_H

ATOM:一种可让相同的字符串只存一次的方案

  • 效果:当程序中申请 new 一个字符串,如果这个字符串之前出现过,就把指针指向之前出现过的那个。
  • 优点:节约空间
  • 注:经常用于字符串,但其它

关键方案

static struct atom {
    struct atom *link; // 哈希冲突的,用链表存下来
    int len;    // 这个字符串的长度
    char *str; // 存放这个字符串
} *buckets[2048];

根据字符串的值,决定其hash值,然后分桶

哈希函数有很多,简单的一个:

for(h=0,i=0;i<len;i++){
    h=(h<<1) + scatter[(unsigned char) str[i]];
}
// 其中 scatter[256] 是一个随机数组,用 rand.h 生成的,它用来帮助hash值更均匀
// (unsigned char) 是因为有的机器char值是带符号的

哈希表(python中的dict)有类似的实现

struct T{
    struct binding{
        struct binding *link;
        const void *key;
        void *value;
    } **buckets;
}

int size;


您的支持将鼓励我继续创作!