C 语言知识点总结

156 阅读12分钟
  • 未定义行为
数组越界

int arr[3] = {1, 2, 3};
printf("%d\n", arr[5]); // 越界访问,结果未定义


解引用空指针

int *ptr = NULL;
printf("%d\n", *ptr); // 解引用空指针,结果未定义

未初始化的局部变量
int x;
printf("%d\n", x); // x 未初始化,结果未定义

浮点数除以零
float x = 1.0;
float y = x / 0.0; // 浮点数除以零,结果未定义

整数除以零
int x = 10;
int y = x / 0; // 整数除以零,结果未定义

符号溢出

signed char x = 127;
x = x + 1; // signed char 溢出,结果未定义

误的类型转换

int *ptr = (int *)malloc(sizeof(int));
float *fptr = (float *)ptr; // 错误的类型转换,结果未定义


内存越界
int *ptr2 =(int *) malloc(sizeof(int) );
free(ptr2);
*ptr2 = 10;


image.png

  • 内存管理

malloc() 函数:用于动态分配内存。它接受一个参数,即需要分配的内存大小(以字节为单位),并返回一个指向分配内存的指针。 calloc() 函数:用于动态分配内存,「并将其初始化为零」。它接受两个参数,即需要分配的内存块数和每个内存块的大小(以字节为单位),并返回一个指向分配内存的指针。

  • 指针变量类型
int  * p = NULL;
指针变量的作用: 
1、决定了指针变量所取空间内容的宽度
2、决定了指针变量 + 1 跳过的单位跨度

void testPointType()
{
    int num = 10;
    int *p = NULL;
    p = #

    // 指针变量的跨度, p是 int 型, p+1 跳过的是 int 占用的 4个字节空间
    printf("&num=%u\n", &num);
     //p1 +1 比 p1 的值大 4
    printf("p=%u\n", p);
    printf("p+1=%u\n", p + 1);

    char *p1 = #
    //p1 +1 比 p1 的值大 1
    printf("p1=%u\n", p1);
    printf("p1+1=%u\n", p1 + 1);
    
     // 如何取出 0x0102
    short *p4 = #
    // p4指向的变量类型是 short,占两个字节,因此 +=1 跳两个字节,由 04 的位置调到 01
    p4 += 1;
    printf("*p4=%#x\n", *p4);
}

  • 变量类型取值
void testPointType2()
{
    void testPointType2()
{
    // 内存中村方式为 04 03 02 01
    int num = 0x01020304;
    int *p1 = #
    // 输出 0x01020304
    printf("*p1=%#x\n", *p1);

    short *p2 = #
    // 由于 short 类型是 2 个字节,因此 *p2智能取出前两个字节,输出 0x0304
    printf("*p2=%#x\n", *p2);

    char *p3 = #
    // 由于 char 类型是 1 个字节,因此 *p3智能取出前 1 个字节,输出 0x04
    printf("*p3=%#x\n", *p3);

    // 如何取出 0x0102
    short *p4 = #
    // p4指向的变量类型是 short,占两个字节,因此 +=1 跳两个字节,由 04 的位置调到 01
    p4 += 1;
    printf("*p4=%#x\n", *p4);

    // 如何取出 0x0203
    char *p5 = #
    // 指向 03
    p5 += 1;
    // 利用 short * 取 2 个字节
    printf("*p5=%#x\n", *(short *)p5);
}

}

  • 在 c 中被调用的函数,必须定义在调用函数的前面,或者是在被调用的函数前面做申明
  • 字符串拼接时,需要先确保添加的字符串容量是否足够,如下代码 str1 空间是 6 个字符(未显示声明时,其容量就是字符长度,包括末尾的 "\0"),在把 str2 拼接到 str1 的末尾时,会报缓存区溢出的错误,原因是 str1的空间不够,将 str1 空间改为 20 就可以了
//修改前
 char str1[] = "hello";
 char str2[] = "world";
 strcat(str1, str2);
     
 //修改后
 char str1[20] = "hello";
 char str2[] = "world";
 strcat(str1, str2);
 
  • 分配了对象后要主动释放,否则会有内存泄漏

  • 没有直接的库函数将字符串改为大写或小写?

  • 需在给定字符数组的大小时在原有的字符串的字符数上加 1。

  • 定义字符串时要指定大小,例如 char title[50]

  • vs 中如何查看 string.h 中库函数的源码

  • 使用 printf的 %x 格式化输出时,打印的数据类型必须和占位符类型一致,否则运行会报错,是否有类似 java 中的 %s 这种能兼容各种数据类型的格式化占位符?

  • 定义变量时,如果赋值的类型和定义的类型不一致时,编译期不会提示,而是等到运行期才会提示 例如以下 5.1 是浮点型,使用 int 去接收,AS 编译期会报错, vs 不会报错

int divisor = 5.1 ;
  • 可变参数

int sum(int count, ...)
{
    int total = 0;
    va_list args;
    // 初始化 args 为可变参数列表
    va_start(args, count);
    for (int i = 0; i < count; i++)
    {
        //va_arg(args, int): 从 va_list 对象中按给定类型获取下一个参数。
        total += va_arg(args, int);
    }
    // 清理工作 清理 va_list 对象,完成可变参数的处理。
    va_end(args);
    return total;
}
  • 使用 printf 输出字符串时,指针不需要解引用

当一个指针作为 printf 函数的参数时,你不需要使用解引用操作符 * 来获取指针指向的值。这是因为 printf 函数的格式字符串会告诉 printf 函数期待的参数类型,而 %s 格式说明符明确指出它期待的是一个指向字符数组(即字符串)的指针。

char *str = "Hello";
int d =10;
//ok
printf("str: %s\n", str); // 使用指针直接传递
//error
printf("str: %s\n", *str); // 使用解引用操作符 *,这是不必要的



  • 使用系统自带的库函数,不会自动导入 .h 文件,需要先手动导入才能使用

  • 获取代码执行耗时时间

#include <time.h>
struct timeval start, end;

    float during;
    // 计算开始的时候,获取当前时间点
    gettimeofday(&start, NULL);
    //代码执行
    func();
    // 计算结束的时候,获取当前时间点
    gettimeofday(&end, NULL);

    during = (end.tv_sec - start.tv_sec) + (end.tv_usec - start.tv_usec);

  • pthread_signal 和 pthread_boraodcast 区别
pthread_signal 一次只能激活一个条件变量, pthread_boraodcast 可以一次激活多个条件变量
  • 变量初始化 char str[], 定义变量如果没有初始化时默认是没有值的(也不是空字符),使用 memset,第 2 个参数必须是 char 类型,如果第 2 个参数是 int 之类的,其实是无效的
     char str[20];
    //将字符串 str 中的字符全部初始化为 0 (空字符)
    memset(str,0,sizeof(str));
    
    //将字符串 str 中的字符全部初始化为 1
    memset(str,1,sizeof(str));
    
    //将字符串 str 中的字符全部初始化为 'a'
    memset(str,’a‘,sizeof(str));
  • 定义字符串变量时要定义大小,例如 char name[20],方括号内要填写大小

  • 在 C 语言中,不能直接使用 = 来给字符数组(如 name)赋字符串值。应该使用 strcpy 函数来复制字符串。

  • 结构体传承的时候,建议传地址,而不是传值,传地址性能会好一些(只会有4个字节),同时为了防止修改指针内容,建议加上 const 关键字,传值的话,相当于把所有参数全部拷贝了一遍

  • C 语言的源代码 -> 预编译 -> 编译 -> 链接 > 可执行程序

预编译: 删除注释, 替换 define 常量, 
  • 删除数组中的某个元素

void removeElement(int arr[], int *size, int index) {
    if (index < 0 || index >= *size) {
        printf("Index out of bounds.\n");
        return;
    }
    for (int i = index; i < *size - 1; i++) {
        arr[i] = arr[i + 1];
    }
    (*size)--; // 减少数组的有效大小
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    int size = sizeof(arr) / sizeof(arr[0]);

    int indexToRemove = 2; // 假设我们要删除索引为 2 的元素,即数字 3
    removeElement(arr, &size, indexToRemove);

    printf("Array after removal: ");
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    return 0;
}
  • static 修饰的变量和函数只能在本文件内使用

  • 堆区、栈区、静态区

 栈区: 局部变量、形参 
 堆区: 动态内存分配,例如使用 malloc 函数
 静态区(数据段):全局变量、静态变量
 代码段:存放函数体(类成员函数和全局函数)的二进制代码
  • 数组初始化
char 
  • malloc 和 calloc 的区别
1、参数不同
2calloc 在返回地址之前把申请的空间的每个字节初始化为 0malloc 不会初始化,因此 malloc 的效率会高一些
  • 运行出现错误时,如何定义错误

image.png

  • 常见的内存分配错误
1、对空指针解引用

2、对动态开辟的内存空间越界访问

3、对非动态开辟内存执行 free

5、对同一块内存多次释放
/**
 * 对同一块内存多次释放
 */
void freeSameMemoryMultiTime(){
    int * ptr =(int *) malloc(19);
    if (ptr == NULL)
    {
        printf("failed to allcoate memory\n");
    }
    free(ptr);
    free(ptr);
    ptr = NULL;
    
}
  • free 释放之后,并不会把指针置为 NULL,需要手动置为 NULL 才行

  • 文件操作模式

image.png

  • 预处理
# 开头的都是预处理指令,例如:
#include. #define 
#define req register

# define reg int a -> register int a


image.png

  • 编译和链接
编译:把源代码编译成目标文件
链接:把目标文件和链接器
  • 使用宏的优劣势
例如比较两个intdouble 数值大小,如果用函数,需要写比较 int 的函数、比较 float 的函数, 但如果使用宏的话就只需要写一遍就行了

劣势:
1、每次石永红宏的时候,一份宏定义的代码将插入到程序中,除非宏比较短,否则可能大幅度增加程序程度
2、宏是无法调试的
3、宏是类型无关,也就不够严谨
4、宏可能会带来运算符优先级的问题,导致容易出现错误

  • 指针访问

void test4()
{
    int aa[2][5] = {1, 2, 3, 4, 5, 6, 7, 8, 9,10};
    /**
     * 1、&aa 二维数组地址
     * 2、&aa +1,跳过整个数组的大小, 也就是数组的最后元素(10)的下一个内存地址
     * 3、ptr1 - 1: 指向 10 这个元素的内存地址
     * 4、*(ptr1 - 1): 对元素 10 这个内存地址解引用,得到 10
     * 
     * 
     */
    int *ptr1 = (int *)(&aa + 1);
    //ptr1 -1 是指到 10 这个地址 ,*(ptr1 - 1) 解引用10这个地址,得到 10
    printf("%d", *(ptr1 - 1));

    /**
     * 1、aa:     数组名,表示首元素地址,也就是第一行的地址, 
     * 2、aa +1:  跳转到第 2行地址,*(aa +1)相当于对第2行解引用,拿到了第 2行, 等价于 aa[1], 
     * 3、*(aa +1):  对第 2 行解引用,相当于是拿到了整个第2行,第 2 行数组名默认是指向首元素地址,
     * 也就是 6 的地址, 对 6 这个地址用 * 解引用,得到的是 6
     * 4、(int *) (*(aa +1)): 使用 int * 强转没有意义,因为  (*(aa +1)) 本来就是一个整型
     * 5、ptr2 -1 : 指向 6 前面一个元素的地址,也就是 5 的地址
     * 6、*(ptr2 -1): 对 5 的地址解引用,得到 5 
     */
    int *ptr2 = (int *)(*(aa + 1));
    //输出 5
    printf("%d", *(ptr2 - 1));
}


void test1()
{
    /**
     * 1、a 是一个素组,数组中的每个元素是 char* (”work“中的 "w", "at"中的 a, "alibaba"中的 a )这3个字符的地址放到了 a 中
     * 2、char** p = a:  a 是数组,默认是指向首元素地址,由于元素类型是 char *, 对 char* 取地址,就是 2级指针,赋值给 har** p 这个二级指针
     * 3、pa++:  pa 本来是指向 "work"这个字符指针的地址,+!:只想 at这个字符指针的地址
     * 4、*pa: 对 pa 解引用,得到的是一级指针,指向 ”at“中的 字符 a 的地址
     * 5、%s打印: 打印从 a 开始的字符串, 输出: at
     */
    char *a[] = {"work", "at", "alibaba"};
    char **pa = a;
    // 指向 at 这个字符指针的地址
    pa++;
    printf("%s\n", *pa);
}

  • 字符比较

str1 和 str2 是两个不同的内存空间,因此 str1 和 str2 是不相等的 str3 和 str4 指向的是常量字符串,在内存中只会有一份,因此指向的都是首字符 h的地址

char str1[] = "hello world";
    char str2[] = "hello world";
    if (str1 == str2)
    {
        printf("str1 and str2 are the same\n");
    }
    else
    {
        printf("str1 and str2 are not the same\n");
    }

    char *str3 = "hello world";
    char *str4 = "hello world";
    if (str3 == str4)
    {
        printf("str3 and str4 are the same\n");
    }
    else
    {
        printf("str3 and str4 are not the same\n");
    }
  • 从数组中查找元素

/**
 * 找到了 return 1;找不到 return 0
 */
int find_num(int arr[3][3], int r, int c, int k)
{
    int x = 0;
    int y = c - 1;
    while (x < r && y >= 0)
    {
        if (arr[x][y] < k)
        {
            // 往下挪动一行
            x++;
        }
        else if (arr[x][y] > k)
        {
            y--;
        }
        else
        {
            printf("找到了: %d\n", k);
            return 1;
        }
    }
    printf("没找到: %d\n", k);
    return 0;
}

/**
 * 找到了 return 1;找不到 return 0
 */
int find_num2(int arr[3][3], int *px, int *py, int k)
{
    int x = 0;
    int y = *py - 1;
    while (x < *px && y >= 0)
    {
        if (arr[x][y] < k)
        {
            // 往下挪动一行
            x++;
        }
        else if (arr[x][y] > k)
        {
            y--;
        }
        else
        {
            *px = x;
            *py = y;
            printf("找到了: %d\n", k);
            return 1;
        }
    }
    printf("没找到: %d\n", k);
    return 0;
}


预处理

  • 条件编译 满足条件编译,不满足条件不编译

#define DEBUG

/**
 * 条件编译,满足某个条件才编译,否则不编译
 */
void conditionCompileTest()
{
    printf("start ---->\n");
#ifdef DEBUG
    printf("process --->\n");
#endif
    printf("end ---->\n");
}

  • 常见的条件编译形式
 1#if  常量表达式
     //...
 #endif
 
 2、 多个分支的常量表达式
 
 #if  常量表达式
    //...
 #elif 常量表达式
    //...
 #else 
    //...
#endif

3、判断是否被定义

#if defined(symbol)
#if !defined(symbol)
#ifdef DEBUG
#ifndef DEBUG

4、嵌套指令

 
  • include <filename.h> 和 "filename.h" 的区别 #include 其实是拷贝一份头文件内容,因此多次引入头文件会导致冗余
 1、<> 是引入库文件的, "" 是引入本地文件的
 2、使用 "xx.h" 引入时会优先从本地文件查找,找不到去标准库函数中查找,因此如果
 函数明确是库函数时,使用 <> 引入的编译效率会更高(省去了从本地文件查找的时间)
 
 
  • 如何避免头文件被重复引入?
方式一、
test.h
ifndef __TEST_H
defin __TEST_H
int Add(int x, int y)
#endif

方式2(比较现代的写法)
#pragma once
int Add(int x, int y)


  • 编译指令 将 test.c编译的产物放到 test.i 文件中
gcc test.c -E > test.i
  • 变量相对于地址的偏移
struct s{
    char c1;
    int a;
    char c2;
};
//变量相对于地址的偏移
#define OFFSET(struct_name, member_name) (size_t)&(((struct_name *)0)->member_name)

int main(int argc, char const *argv[])
{
    printf("%zu\n",OFFSET(struct s,c1));
    printf("%zu\n",OFFSET(struct s,a));
    printf("%zu\n",OFFSET(struct s,c2));
    return 0;
}