Posts 聊聊 C 中没注意的一些特性
Post
Cancel

聊聊 C 中没注意的一些特性

  1. C 中extern 关键字?

    extern 用于声明一个变量或函数是“外部的”,也就是在其他文件中定义的。它不会分配内存,只是告诉编译器这个变量或函数在其他文件中定义,可以在当前文件中使用。

    C 不存在global 关键字!

  2. 头文件编译链接问题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    
     // test.c
     #include "cutil.h"
    
    
     int main() {
         char s[MAXLINE];
    
         int i = getlines(s);
         printf("%s", s);
         return 0;
     }
    
    
     // cutil.h
     #ifndef CUTIL_H
     #define CUTIL_H
    
     #include <stdio.h>
     #define MAXLINE 20
    
     int getlines(char[]);
    
     #endif
    
     // util.c
     #include "cutil.h"
    
     int getlines(char lines[]) {
         char c;
         int i = 0;
    
         while ((c = getchar()) != 'A') {
             if (i >= MAXLINE-1) {
                 continue;
             }
    
             lines[i++] = c;
         }
    
         lines[i] = '\0';
    
         return i;
     }
    
     // 编译链接
     gcc test.c cutil.c -o test
    

    头文件本身不直接参与编译,它通过 #include 将声明插入到需要的源文件中,参与编译的依然是 .c 文件。

  3. 内联函数

    内联函数(inline)是为了提高性能,建议编译器将函数体直接插入到调用点,而不是通过函数调用机制(如压栈、跳转等)执行。

  4. 为什么c 中, char *s 字符串指针内容不可更改, char [] 数组可以更改 char *str = “hello”; 这行代码的行为并不是将字符串存储在栈区。”hello” 是一个字符串字面量,它存储在程序的只读数据段(通常是 .rodata 或类似的区域)中。这个区域是不可修改的,专门用来存储常量数据。char *str 是一个指针变量,它存储在栈区(或局部变量所在的内存区域)。这个指针变量指向字符串字面量 “hello” 在内存中的地址。

    当你定义 char s[] = “hello”; 时,s 是一个字符数组,它会存储在栈区。这个数组的大小是由字符串字面量的长度决定的,即 strlen(“hello”) + 1 = 6,包括字符串结尾的空字符 ‘\0’。由于数组是可修改的,因此可以在后续的代码中改变 s 数组的内容。

    字符串常量不可修改!!!!

  5. 这句话好难理解, 《如果变量不是自动变量, 则只能进行一次初始化操作》。

    “初始化” 这个词在 C 语言中的特殊含义,特别是它在静态变量、全局变量、自动变量(局部变量)中的行为。 初始化在 C 语言中的意思是 在变量声明时给它一个初始值,也就是为变量分配一个初始的数值,通常是通过赋值语句来完成。 在 C 语言中,变量的初始化和修改值是两个不同的概念,理解这两者之间的区别非常重要。我们强调“初始化”是因为在某些类型的变量中,初始化有严格的规则,且它只能发生一次。 对于 静态变量、全局变量 和 动态分配的内存,它们的生命周期是 整个程序执行过程中的某个时间段,这就决定了初始化操作在它们生命周期内只应该执行一次。如果在程序的其他地方再进行初始化操作,就会产生歧义和潜在的问题。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    
     // 自动变量(局部变量)会在每次进入作用域时进行初始化。每次函数被调用时,局部变量都会重新初始化,并且它的值从头开始。
     void func() {
     int x = 5;  // 初始化
     printf("%d\n", x);  // 输出 5
     x = 10;  // 修改值
     printf("%d\n", x);  // 输出 10
     }
    
     // 静态变量和全局变量的初始化只会发生一次,它们在程序开始时初始化,在程序结束时销毁。它们会在第一次使用时初始化,之后会保留上次的值,不能再被重新初始化。
     void func() {
     static int x = 5;  // 初始化(只会初始化一次)
     x++;
     printf("%d\n", x);  // 每次调用会输出 x 的增值
     } 
    
     ---
     int x = 10;  // 全局变量,只初始化一次
    
     void func() {
         printf("%d\n", x);  // 输出 10
         x = 20;             // 修改值
         printf("%d\n", x);  // 输出 20
     }
    
     // 动态内存分配的变量(通过 malloc 或 calloc)也只能在分配内存时进行初始化一次。之后可以修改这些变量的值,但不能“重新初始化”。
     int* ptr = (int*)malloc(sizeof(int));  // 动态分配内存,初始化
     *ptr = 10;  // 修改值
     free(ptr);  // 释放内存
    

    C 语言要求 静态变量、全局变量 和 动态分配内存的变量 在它们的生命周期内 只能初始化一次,这是为了保证变量的值在程序执行过程中保持一致。再进行初始化会导致程序的行为不确定或产生错误。

  6. 字符串定义方式对比

定义方式内存分配位置是否可修改内容是否涉及字符串常量备注
char s[] = "hello world";栈上(或全局变量区)可以是,内容从字符串常量复制到数组中分配了独立的内存,内容可修改,不影响字符串常量
char s[11] = "hello world"; 是数组栈上(或全局变量区)可以是,内容从字符串常量复制到数组中手动指定了数组大小,需确保长度 >= 字符串+1,否则会编译报错或溢出
char *s = "hello world"; 是指针静态只读内存区(字符串常量区)不可以是,指针指向字符串常量指针指向的是只读区域,尝试修改内容会导致未定义行为
char *s = malloc(...); s = "hello world";堆上分配(指针指向堆区)不可以是,指针被重新指向字符串常量原指针动态分配的堆内存会变得不可用(导致内存泄漏),一般是错误的用法(这是个错误写法)

  1. static & extern 关键字
关键字作用范围作用对象影响适用场景
static(全局变量)当前 .c 文件变量变量仅限当前文件,不能被其他文件访问需要在当前文件中保持私有状态的全局变量
static(局部变量)当前函数变量变量在函数调用间保持值不变(存储在静态存储区)需要在函数内部保持状态
static(函数)当前 .c 文件函数仅能在当前文件内部调用,不会暴露给其他文件仅在当前 .c 文件使用的工具函数
static(在 .h 文件)每个包含该 .h.c 文件都会有独立副本变量/函数不能在多个 .c 文件间共享,会导致代码膨胀避免在 .h 里使用 static,除非需要每个 .c 文件独立实例

extern 关键字:

  1. 一般声明在头文件里。 (extern x);
  2. 在 .c 文件中赋值。
  3. 全部 .c 文件 都可以引用。

  4. 数组指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// 1. 数组名作为参数传递时会退化为指针
//在 C 语言中,当数组名作为函数参数传递时,它会退化为指向数组首元素的指针。这意味着函数内部接收的是一个指针,而不是整个数组。

#include <stdio.h>

void print_string(char s[]) {
    printf("%s\n", s);
}

int main() {
    char s[12] = "hello world";
    print_string(s); // 数组名 s 退化为指针
    return 0;
}

/*
2. 在 C 语言中,所有函数参数都是值传递的,包括指针。这意味着传递给函数的是指针的副本,而不是指针本身。
*/
#include <stdio.h>

void modify_pointer(char *s) {
    s++; // 修改指针的副本
    printf("Inside function: %s\n", s);
}

int main() {
    char s[12] = "hello world";
    modify_pointer(s); // 传递指针的副本
    printf("Outside function: %s\n", s);
    return 0;
}

/*
3. 在函数内部,s 是指针的副本,可以对其进行操作(如 s++)。这种操作只会影响函数内部的副本,不会影响外部的指针。
*/

#include <stdio.h>

void print_string(char *s) {
    while (*s != '\0') {
        printf("%c", *s);
        s++; // 合法,修改指针的副本
    }
    printf("\n");
}

int main() {
    char s[12] = "hello world";
    print_string(s);
    return 0;
}

/*
4.在函数外,数组名是一个指向数组首元素的常量指针,不能被修改。因此,s++ 或 ++s 在函数外是非法的
*/
#include <stdio.h>

int main() {
    char s[12] = "hello world";
    // s++; // 非法,数组名是常量指针
    // ++s; // 非法,数组名是常量指针
    char *p = s; // 合法,将数组名赋值给指针变量
    p++; // 合法,修改指针变量
    printf("%s\n", p);
    return 0;
}

/*
5. 如果需要在函数内部修改外部的指针,可以传递指针的指针。
*/
#include <stdio.h>

void modify_pointer(char **s) {
    (*s)++; // 修改外部的指针
}

int main() {
    char s[12] = "hello world";
    char *p = s;
    modify_pointer(&p); // 传递指针的指针
    printf("%s\n", p);
    return 0;
}
  1. 数组指针 & 指针数组 & 多为数组
名称定义作用示例访问方式
普通指针(Pointer to Element)int *p;指向数组的某个元素int arr[5] = {1, 2, 3, 4, 5}; int *p = arr;*p 访问当前元素,p++ 移动到下一个元素
数组指针(Pointer to an Array)int (*p)[5];指向一个完整的数组int arr[5] = {1, 2, 3, 4, 5}; int (*p)[5] = &arr;(*p)[i] 访问数组中的元素 ,p++ 会跳过整个数组 arr(指针移动 5 * sizeof(int))。
指针数组(Array of Pointers)int *p[5];一个数组,每个元素都是指针int a=1, b=2; int *p[2] = {&a, &b};*p[i] 访问指向的值

指针数组的初始化: char *z[] = {"aa", "bb"};

代码含义结构
int a[10][20];二维数组,包含 10 个长度为 20 的 int 类型数组。a 是一个二维数组,可以理解为 int a[10][20] 这样的矩阵。
int *b[20];指针数组,包含 20 个 int * 指针,每个指针可以指向一个 int 变量或 int 数组。b 是一个存储指针的数组,每个元素是 int * 类型的指针。

指针数组的最常用法:char *z[3] = {"aaaa", "bbb", "c"}; // 不同长度str 的数组

  1. 函数指针
语法作用示例
int (*f)();f 是指向返回 int函数指针int (*f)(); f = myFunction; int result = f();
int *f();f返回 int* 的函数int *f() { static int x = 10; return &x; }
int (*ops[])(int, int);函数指针数组,用于存储多个函数地址int (*ops[])(int, int) = { add, sub, mul }; int res = ops[0](5, 3);
typedef int (*operation)(int, int);使用 typedef 简化函数指针定义typedef int (*op)(int, int); op myOp = add; int res = myOp(2, 3);
qsort(arr, size, sizeof(int), compare);compare 作为回调函数,决定排序规则int compare(const void *a, const void *b) { return (*(int*)a - *(int*)b); }
int (*getOperation(char op))(int, int);指向返回函数指针的函数int (*getOperation(char op))(int, int) { return (op == '+') ? add : sub; }
  1. 有关void *
特点说明示例
通用指针void * 可指向任意类型的数据c void *ptr; int x = 10; ptr = &x;
不能解引用需要先转换成具体类型c int val = *(int *)ptr;
不能指针算术不能 ptr++,必须先转换c int *ip = (int *)ptr; ip++;
动态内存分配malloc() 返回 void *,需要转换c int *p = (int *)malloc(5 * sizeof(*p));
回调函数qsort()bsearch() 传递 void *c int compare(const void *a, const void *b);
数据结构链表、哈希表使用 void * 存储不同类型的数据c typedef struct { void *data; } Node;
  1. 结构体
  • 结构定义
1
2
3
4
5
6
7
8
9
10
11
    struct test {
        int x;
    }y,z,g;

== 

    struct test {
        int x;
    };

    struct test y, z, g;
  • 自引用结构

自引用结构体(Self-referential Struct)是指结构体内部包含一个指向自身类型的指针。这种结构在链表、树、图等数据结构中非常常见。

1
2
3
4
5
// struct Node *next; 不能写成 struct Node next;,否则会导致递归定义,编译失败。
struct Node {
    int data;
    struct Node *next;  // 指向自身类型的指针
};
  • 匿名结构体
1
2
3
4
5
struct {
    int x;
    int y;
} point;

  • 结构体别名
1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct test {
    int x;
} y;

struct test var1;  // 传统方式
y var2;            // 使用 typedef 别名


// 更简便的一种写法
typedef struct {
    int x;
    int y;
} Point;  // 直接使用 Point 代替 struct {...}
  1. 位字段

在 C 语言中,位字段是结构体中用来节省内存的机制,它允许你指定结构体成员的精确位宽。位字段通常用于存储多个标志位(如二进制开关),或者需要精确控制内存大小的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct Example {
    int a : 5;   // 5 位宽,最多能存储 32 的整数
    int b : 3;   // 3 位宽,最多能存储 8 的整数
    int c : 10;  // 10 位宽,最多能存储 1024 的整数
};



//在这个结构体中,a、b 和 c 只占用 1 位,但它们是 unsigned int 类型的位字段。为了满足对齐要求,编译器会为它们分配 4 字节内存,并插入填充字节。
struct Example {
    unsigned int a : 1;   // 1 位
    unsigned int b : 1;   // 1 位
    unsigned int c : 1;   // 1 位
};


// 哪还有什么意义 ?

位字段的意义不在于节省内存,而是在于:

提供精确的控制,特别是用于表示硬件寄存器的标志位;
压缩数据,使得多个小范围的值能够合并在一个数据单元中;
映射固定格式的二进制数据,如网络协议;
提升代码可读性,使得复杂的数据结构更加简洁明了。
  1. 静态 & 全局变量再说明

总览说明: 静态变量 & 全局变量 储存区都是一样的。 在c语言中, 编译器对初始化操作符 = 和赋值操作符 = 的处理是不同的。初始化操作符 =:用于在声明变量时初始化,必须使用常量表达式。赋值操作符 =:用于在程序运行时赋值,可以使用函数调用(如 malloc)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 1. 初始化静态或全局变量时不能调用函数

// 2. 对于静态变量,可以通过在单独的语句中使用 malloc 分配内存来解决问题。这是因为静态变量的生命周期是程序运行期间,可以在函数中对其进行赋值。
static int *pi;          // 声明静态变量
pi = malloc(sizeof(int)); // 在函数中分配内存

// 3. 全局变量与静态变量不同,它们是在函数外部声明的,因此不能直接在全局作用域中使用赋值语句(如 pi = malloc(sizeof(int));)。赋值语句必须出现在函数中。

// 3.1 全局变量可以在声明时进行初始化,但不能使用赋值语句。这是因为赋值语句是执行时的操作,而全局变量的初始化必须在编译时完成。
int global_var = 42; // 正确:声明时初始化
---
int global_var;
global_var = 42; // 错误:不能在全局作用域中使用赋值语句

// 3.2 动态内存分配(如 malloc)是运行时的操作,因此不能在全局作用域中直接调用。
int *pi = malloc(sizeof(int)); // 错误:不能在全局作用域中调用函数
-----
int *pi;
pi = malloc(sizeof(int));      // 错误:不能在全局作用域中使用赋值语句

// 3.3 正确方法 , 在函数中动态分配
int *pi; // 声明全局变量

void init_global() {
    pi = malloc(sizeof(int)); // 在函数中分配内存
}

// 3.4 静态变量(包括全局静态变量和函数内部的静态变量)的初始化规则与全局变量类似。
static int static_global_var = 42; // 正确:声明时初始化
-----
void func() {
    static int static_local_var = 42; // 正确:声明时初始化
}
------
static int static_global_var;
static_global_var = 42; // 错误:不能在全局作用域中使用赋值语句
This post is licensed under CC BY 4.0 by the author.

Contents

Trending Tags