C 中extern 关键字?
extern 用于声明一个变量或函数是“外部的”,也就是在其他文件中定义的。它不会分配内存,只是告诉编译器这个变量或函数在其他文件中定义,可以在当前文件中使用。
C 不存在global 关键字!
头文件编译链接问题
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 文件。
内联函数
内联函数(inline)是为了提高性能,建议编译器将函数体直接插入到调用点,而不是通过函数调用机制(如压栈、跳转等)执行。
为什么c 中, char *s 字符串指针内容不可更改, char [] 数组可以更改 char *str = “hello”; 这行代码的行为并不是将字符串存储在栈区。”hello” 是一个字符串字面量,它存储在程序的只读数据段(通常是 .rodata 或类似的区域)中。这个区域是不可修改的,专门用来存储常量数据。char *str 是一个指针变量,它存储在栈区(或局部变量所在的内存区域)。这个指针变量指向字符串字面量 “hello” 在内存中的地址。
当你定义 char s[] = “hello”; 时,s 是一个字符数组,它会存储在栈区。这个数组的大小是由字符串字面量的长度决定的,即 strlen(“hello”) + 1 = 6,包括字符串结尾的空字符 ‘\0’。由于数组是可修改的,因此可以在后续的代码中改变 s 数组的内容。
字符串常量不可修改!!!!
这句话好难理解, 《如果变量不是自动变量, 则只能进行一次初始化操作》。
“初始化” 这个词在 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 语言要求 静态变量、全局变量 和 动态分配内存的变量 在它们的生命周期内 只能初始化一次,这是为了保证变量的值在程序执行过程中保持一致。再进行初始化会导致程序的行为不确定或产生错误。
字符串定义方式对比
| 定义方式 | 内存分配位置 | 是否可修改内容 | 是否涉及字符串常量 | 备注 |
|---|---|---|---|---|
char s[] = "hello world"; | 栈上(或全局变量区) | 可以 | 是,内容从字符串常量复制到数组中 | 分配了独立的内存,内容可修改,不影响字符串常量 |
char s[11] = "hello world"; 是数组 | 栈上(或全局变量区) | 可以 | 是,内容从字符串常量复制到数组中 | 手动指定了数组大小,需确保长度 >= 字符串+1,否则会编译报错或溢出 |
char *s = "hello world"; 是指针 | 静态只读内存区(字符串常量区) | 不可以 | 是,指针指向字符串常量 | 指针指向的是只读区域,尝试修改内容会导致未定义行为 |
char *s = malloc(...); s = "hello world"; | 堆上分配(指针指向堆区) | 不可以 | 是,指针被重新指向字符串常量 | 原指针动态分配的堆内存会变得不可用(导致内存泄漏),一般是错误的用法(这是个错误写法) |
- static & extern 关键字
| 关键字 | 作用范围 | 作用对象 | 影响 | 适用场景 |
|---|---|---|---|---|
static(全局变量) | 当前 .c 文件 | 变量 | 变量仅限当前文件,不能被其他文件访问 | 需要在当前文件中保持私有状态的全局变量 |
static(局部变量) | 当前函数 | 变量 | 变量在函数调用间保持值不变(存储在静态存储区) | 需要在函数内部保持状态 |
static(函数) | 当前 .c 文件 | 函数 | 仅能在当前文件内部调用,不会暴露给其他文件 | 仅在当前 .c 文件使用的工具函数 |
static(在 .h 文件) | 每个包含该 .h 的 .c 文件都会有独立副本 | 变量/函数 | 不能在多个 .c 文件间共享,会导致代码膨胀 | 避免在 .h 里使用 static,除非需要每个 .c 文件独立实例 |
extern 关键字:
- 一般声明在头文件里。 (extern x);
- 在 .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
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;
}
- 数组指针 & 指针数组 & 多为数组
| 名称 | 定义 | 作用 | 示例 | 访问方式 |
|---|---|---|---|---|
| 普通指针(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 的数组
- 函数指针
| 语法 | 作用 | 示例 |
|---|---|---|
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; } |
- 有关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
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 {...}
- 位字段
在 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 位
};
// 哪还有什么意义 ?
位字段的意义不在于节省内存,而是在于:
提供精确的控制,特别是用于表示硬件寄存器的标志位;
压缩数据,使得多个小范围的值能够合并在一个数据单元中;
映射固定格式的二进制数据,如网络协议;
提升代码可读性,使得复杂的数据结构更加简洁明了。
- 静态 & 全局变量再说明
总览说明: 静态变量 & 全局变量 储存区都是一样的。 在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; // 错误:不能在全局作用域中使用赋值语句