类和内存
# 09.类和内存
# 目录介绍
# 9.1 面向对象设计
# 9.1.0 没有类概念
C 语言没有类的概念,但可以通过结构体和函数指针模拟面向对象的特性。
- 使用结构体封装数据,函数指针模拟成员方法。
- 通过嵌套结构体模拟继承,通过函数指针和类型转换模拟多态。
虽然 C 语言不是面向对象的语言,但通过这些技巧可以实现类似的功能。
# 9.1.1 结构体模拟类
结构体可以用来封装数据,类似于类中的成员变量。但是这种类只有成员变量……
示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 定义一个结构体模拟类
typedef struct {
char name[50];
int age;
} Person;
// 构造函数
Person* Person_create(const char *name, int age) {
Person *p = (Person *)malloc(sizeof(Person));
if (p != NULL) {
strcpy(p->name, name);
p->age = age;
}
return p;
}
// 成员函数
void Person_introduce(Person *p) {
printf("Hello, my name is %s and I am %d years old.\n", p->name, p->age);
}
// 析构函数
void Person_destroy(Person *p) {
free(p);
}
int main() {
// 创建对象
Person *person = Person_create("Alice", 25);
// 调用成员函数
Person_introduce(person);
// 销毁对象
Person_destroy(person);
return 0;
}
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
# 9.1.2 模拟成员方法
通过将函数指针作为结构体的成员,可以模拟类的成员方法。
示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 定义一个结构体模拟类
typedef struct {
char name[50];
int age;
void (*introduce)(struct Person *); // 函数指针
} Person;
// 成员函数
void Person_introduce(Person *p) {
printf("Hello, my name is %s and I am %d years old.\n", p->name, p->age);
}
// 构造函数
Person* Person_create(const char *name, int age) {
Person *p = (Person *)malloc(sizeof(Person));
if (p != NULL) {
strcpy(p->name, name);
p->age = age;
p->introduce = Person_introduce; // 初始化函数指针
}
return p;
}
// 析构函数
void Person_destroy(Person *p) {
free(p);
}
int main() {
// 创建对象
Person *person = Person_create("Bob", 30);
// 调用成员函数
person->introduce(person);
// 销毁对象
Person_destroy(person);
return 0;
}
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
# 9.1.3 模拟继承
通过嵌套结构体,可以模拟继承的特性。
示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 基类
typedef struct {
char name[50];
int age;
} Person;
// 派生类
typedef struct {
Person base; // 继承基类
char job[50];
} Employee;
// 基类构造函数
Person* Person_create(const char *name, int age) {
Person *p = (Person *)malloc(sizeof(Person));
if (p != NULL) {
strcpy(p->name, name);
p->age = age;
}
return p;
}
// 派生类构造函数
Employee* Employee_create(const char *name, int age, const char *job) {
Employee *e = (Employee *)malloc(sizeof(Employee));
if (e != NULL) {
strcpy(e->base.name, name);
e->base.age = age;
strcpy(e->job, job);
}
return e;
}
// 基类成员函数
void Person_introduce(Person *p) {
printf("Hello, my name is %s and I am %d years old.\n", p->name, p->age);
}
// 派生类成员函数
void Employee_introduce(Employee *e) {
Person_introduce((Person *)e); // 调用基类方法
printf("I work as a %s.\n", e->job);
}
// 析构函数
void Person_destroy(Person *p) {
free(p);
}
int main() {
// 创建派生类对象
Employee *employee = Employee_create("Charlie", 35, "Engineer");
// 调用派生类成员函数
Employee_introduce(employee);
// 销毁对象
Person_destroy((Person *)employee);
return 0;
}
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
# 9.1.4 模拟多态
通过函数指针和类型转换,可以模拟多态的特性。
示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 基类
typedef struct {
void (*introduce)(void *); // 函数指针
} Animal;
// 派生类:Dog
typedef struct {
Animal base; // 继承基类
char name[50];
} Dog;
// 派生类:Cat
typedef struct {
Animal base; // 继承基类
char name[50];
} Cat;
// Dog 的成员函数
void Dog_introduce(void *dog) {
Dog *d = (Dog *)dog;
printf("Woof! My name is %s.\n", d->name);
}
// Cat 的成员函数
void Cat_introduce(void *cat) {
Cat *c = (Cat *)cat;
printf("Meow! My name is %s.\n", c->name);
}
// Dog 构造函数
Dog* Dog_create(const char *name) {
Dog *d = (Dog *)malloc(sizeof(Dog));
if (d != NULL) {
strcpy(d->name, name);
d->base.introduce = Dog_introduce; // 初始化函数指针
}
return d;
}
// Cat 构造函数
Cat* Cat_create(const char *name) {
Cat *c = (Cat *)malloc(sizeof(Cat));
if (c != NULL) {
strcpy(c->name, name);
c->base.introduce = Cat_introduce; // 初始化函数指针
}
return c;
}
// 析构函数
void Animal_destroy(Animal *a) {
free(a);
}
int main() {
// 创建对象
Dog *dog = Dog_create("Buddy");
Cat *cat = Cat_create("Whiskers");
// 调用多态方法
Animal *animals[] = {(Animal *)dog, (Animal *)cat};
for (int i = 0; i < 2; i++) {
animals[i]->introduce(animals[i]);
}
// 销毁对象
Animal_destroy((Animal *)dog);
Animal_destroy((Animal *)cat);
return 0;
}
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
# 9.1.5 综合案例与思考
综合案例:用C语言实现面向对象的形状系统
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
// "基类":Shape
typedef struct Shape {
const char *type;
double (*area)(struct Shape *self);
double (*perimeter)(struct Shape *self);
void (*print)(struct Shape *self);
} Shape;
// "派生类":Circle
typedef struct {
Shape base;
double radius;
} Circle;
// "派生类":Rectangle
typedef struct {
Shape base;
double width, height;
} Rectangle;
// Circle的方法实现
double circle_area(Shape *self) {
Circle *c = (Circle *)self;
return 3.14159 * c->radius * c->radius;
}
double circle_perimeter(Shape *self) {
Circle *c = (Circle *)self;
return 2 * 3.14159 * c->radius;
}
void circle_print(Shape *self) {
Circle *c = (Circle *)self;
printf("圆(半径=%.1f): 面积=%.2f, 周长=%.2f\n",
c->radius, self->area(self), self->perimeter(self));
}
// Rectangle的方法实现
double rect_area(Shape *self) {
Rectangle *r = (Rectangle *)self;
return r->width * r->height;
}
double rect_perimeter(Shape *self) {
Rectangle *r = (Rectangle *)self;
return 2 * (r->width + r->height);
}
void rect_print(Shape *self) {
Rectangle *r = (Rectangle *)self;
printf("矩形(%.1f x %.1f): 面积=%.2f, 周长=%.2f\n",
r->width, r->height, self->area(self), self->perimeter(self));
}
// 构造函数
Circle *Circle_new(double radius) {
Circle *c = (Circle *)malloc(sizeof(Circle));
c->base.type = "Circle";
c->base.area = circle_area;
c->base.perimeter = circle_perimeter;
c->base.print = circle_print;
c->radius = radius;
return c;
}
Rectangle *Rect_new(double w, double h) {
Rectangle *r = (Rectangle *)malloc(sizeof(Rectangle));
r->base.type = "Rectangle";
r->base.area = rect_area;
r->base.perimeter = rect_perimeter;
r->base.print = rect_print;
r->width = w;
r->height = h;
return r;
}
int main() {
// 多态:用基类指针数组存储不同类型
Shape *shapes[3];
shapes[0] = (Shape *)Circle_new(5.0);
shapes[1] = (Shape *)Rect_new(4.0, 6.0);
shapes[2] = (Shape *)Circle_new(3.0);
printf("=== 多态调用 ===\n");
double total_area = 0;
for (int i = 0; i < 3; i++) {
shapes[i]->print(shapes[i]);
total_area += shapes[i]->area(shapes[i]);
}
printf("总面积: %.2f\n", total_area);
// 释放内存
for (int i = 0; i < 3; i++) free(shapes[i]);
return 0;
}
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
84
85
86
87
88
89
90
91
92
93
94
95
96
原理说明:这个案例展示了C语言实现面向对象三大特性的完整方法:封装(结构体包含数据和函数指针)、继承(派生结构体的第一个成员是基类结构体,利用内存布局兼容性进行类型转换)、多态(通过函数指针实现虚函数表,不同类型有不同的实现)。这正是GTK、Linux内核等大型C项目的编程模式。C++的虚函数机制在底层也是类似的原理。
思考题:
- 为什么派生结构体的"基类"必须是第一个成员?如果放在其他位置会怎样?
- 这种手动实现的多态与C++的虚函数有什么异同?C++编译器是如何实现虚函数表的?
- 如何为这个形状系统添加一个"析构函数"机制,确保不同类型能正确清理各自的资源?
# 9.2.1 栈(Stack)
用于存储局部变量、函数参数和函数调用的上下文。
- 内存由编译器自动分配和释放。
- 大小有限,通常较小(几 MB)。
- 访问速度快。
示例:
void func() {
int x = 10; // x 存储在栈中
}
2
3
# 9.2.2 堆(Heap)
用于动态内存分配。
- 内存由程序员手动管理(使用
malloc、calloc、realloc和free)。 - 大小较大,受系统内存限制。
- 访问速度较慢。
示例:
int *p = (int *)malloc(sizeof(int)); // 在堆中分配内存
free(p); // 释放内存
2
# 9.2.3 全局/静态区
用于存储全局变量和静态变量。
- 内存由编译器在程序启动时分配,在程序结束时释放。
- 分为初始化和未初始化两部分。
示例:
int global_var = 10; // 全局变量,存储在全局区
static int static_var = 20; // 静态变量,存储在全局区
2
# 9.2.4 常量区
用于存储字符串常量和 const 变量。
- 内存由编译器分配,程序结束时释放。
- 通常是只读的。
示例:
const char *str = "Hello, World!"; // 字符串常量,存储在常量区
# 9.2.5 代码区
- 用于存储程序的二进制代码(指令)。
- 通常是只读的。
# 9.2.6 综合案例与思考
综合案例:探索C程序的内存布局
#include <stdio.h>
#include <stdlib.h>
// 全局变量 —— 全局/静态区
int global_init = 100; // 已初始化的全局变量
int global_uninit; // 未初始化的全局变量(BSS段)
static int static_global = 200;
// 常量 —— 常量区
const char *str_literal = "Hello, Memory!";
void show_memory_layout() {
// 局部变量 —— 栈
int local_var = 42;
static int static_local = 300; // 静态区(非栈)
// 动态分配 —— 堆
int *heap_var = (int *)malloc(sizeof(int));
*heap_var = 999;
printf("=== C程序内存布局 ===\n\n");
printf("代码区(函数地址):\n");
printf(" main() = %p\n", (void *)main);
printf(" show_memory() = %p\n\n", (void *)show_memory_layout);
printf("常量区:\n");
printf(" 字符串字面量 = %p\n\n", (void *)str_literal);
printf("全局/静态区:\n");
printf(" global_init = %p (值=%d)\n", (void *)&global_init, global_init);
printf(" global_uninit = %p (值=%d)\n", (void *)&global_uninit, global_uninit);
printf(" static_global = %p (值=%d)\n", (void *)&static_global, static_global);
printf(" static_local = %p (值=%d)\n\n", (void *)&static_local, static_local);
printf("栈区(从高地址向低地址增长):\n");
printf(" local_var = %p (值=%d)\n", (void *)&local_var, local_var);
printf(" heap_var(指针) = %p\n\n", (void *)&heap_var);
printf("堆区(从低地址向高地址增长):\n");
printf(" *heap_var = %p (值=%d)\n", (void *)heap_var, *heap_var);
free(heap_var);
}
int main() {
show_memory_layout();
return 0;
}
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
原理说明:C程序的内存布局从低地址到高地址依次是:代码区(.text)、常量区(.rodata)、已初始化全局/静态变量区(.data)、未初始化全局/静态变量区(.bss)、堆(向高地址增长)、栈(向低地址增长)。堆和栈之间有一大块空闲区域。栈的大小在程序启动时就确定了(通常几MB),而堆可以动态扩展。static 局部变量虽然写在函数内部,但实际存储在全局/静态区,生命周期与程序相同。
思考题:
- 运行上面的程序,观察各区域的地址,能否验证内存布局从低到高的顺序?
- 栈和堆如果增长方向相向而行,是否可能发生碰撞?这种情况叫什么?
static局部变量和全局变量在内存位置上有什么区别?它们的初始化时机分别是什么?
动态内存管理使用 malloc、calloc、realloc 和 free。
# 9.3.1 malloc
- 分配指定大小的内存块。
- 返回指向分配内存的指针。
- 分配的内存未初始化。
示例:
int *p = (int *)malloc(sizeof(int)); // 分配 4 字节内存
if (p == NULL) {
perror("Failed to allocate memory");
return 1;
}
*p = 10;
free(p); // 释放内存
2
3
4
5
6
7
# 9.3.2 calloc
- 分配指定数量和大小的内存块。
- 返回指向分配内存的指针。
- 分配的内存初始化为 0。
示例:
int *p = (int *)calloc(5, sizeof(int)); // 分配 5 个 int 大小的内存
if (p == NULL) {
perror("Failed to allocate memory");
return 1;
}
free(p); // 释放内存
2
3
4
5
6
# 9.3.3 realloc
- 调整已分配内存块的大小。
- 返回指向新内存块的指针。
- 如果新大小大于原大小,新增部分未初始化。
示例:
int *p = (int *)malloc(5 * sizeof(int)); // 分配 5 个 int 大小的内存
p = (int *)realloc(p, 10 * sizeof(int)); // 调整为 10 个 int 大小
if (p == NULL) {
perror("Failed to reallocate memory");
return 1;
}
free(p); // 释放内存
2
3
4
5
6
7
# 9.3.4 free
- 释放动态分配的内存。
- 只能释放由
malloc、calloc或realloc分配的内存。
示例:
int *p = (int *)malloc(sizeof(int));
free(p); // 释放内存
2
# 9.3.5 综合案例与思考
综合案例:动态内存管理实战——可增长的动态数组
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
int *data;
int size; // 当前元素个数
int capacity; // 当前容量
} DynArray;
// 创建动态数组
DynArray *dynarray_create(int init_cap) {
DynArray *arr = (DynArray *)malloc(sizeof(DynArray));
if (!arr) return NULL;
arr->data = (int *)calloc(init_cap, sizeof(int));
if (!arr->data) { free(arr); return NULL; }
arr->size = 0;
arr->capacity = init_cap;
return arr;
}
// 添加元素(自动扩容)
int dynarray_push(DynArray *arr, int value) {
if (arr->size >= arr->capacity) {
int new_cap = arr->capacity * 2;
int *new_data = (int *)realloc(arr->data, new_cap * sizeof(int));
if (!new_data) return -1;
arr->data = new_data;
arr->capacity = new_cap;
printf(" [扩容: %d -> %d]\n", arr->capacity / 2, new_cap);
}
arr->data[arr->size++] = value;
return 0;
}
// 打印
void dynarray_print(DynArray *arr) {
printf("数组(size=%d, cap=%d): ", arr->size, arr->capacity);
for (int i = 0; i < arr->size; i++) printf("%d ", arr->data[i]);
printf("\n");
}
// 销毁
void dynarray_destroy(DynArray *arr) {
if (arr) {
free(arr->data);
arr->data = NULL;
free(arr);
}
}
int main() {
DynArray *arr = dynarray_create(4);
printf("=== 动态数组演示 ===\n");
for (int i = 1; i <= 10; i++) {
dynarray_push(arr, i * 10);
}
dynarray_print(arr);
dynarray_destroy(arr);
printf("内存已释放\n");
return 0;
}
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
原理说明:malloc 从堆上分配内存但不初始化(内容是垃圾值);calloc 分配并初始化为0;realloc 调整已分配内存的大小,可能在原地扩展或移动到新位置。realloc 成功时返回新指针(可能与原指针不同),失败返回 NULL——注意不要直接写 p = realloc(p, ...), 否则失败时原内存地址丢失,造成内存泄漏。每次倍增容量(capacity * 2)是经典策略,保证 n 次插入的均摊时间复杂度为 O(1)。
思考题:
realloc(p, new_size)在什么情况下会返回与p不同的地址?这对程序有什么影响?- 为什么动态数组扩容通常选择2倍而不是每次+1?从时间复杂度角度分析。
malloc(0)返回什么?realloc(NULL, size)等价于什么?realloc(p, 0)呢?
常见内存问题包括内存泄漏、野指针、双重释放和越界访问。
# 9.5.1 内存泄漏
- 动态分配的内存未释放。
- 导致程序占用内存不断增加。
示例:
void func() {
int *p = (int *)malloc(sizeof(int));
// 忘记调用 free(p);
}
2
3
4
# 9.5.2 野指针
- 指针指向已释放的内存。
- 访问野指针会导致未定义行为。
示例:
int *p = (int *)malloc(sizeof(int));
free(p);
*p = 10; // 野指针访问
2
3
# 9.5.3 双重释放
- 对同一块内存多次调用
free。 - 导致程序崩溃。
示例:
int *p = (int *)malloc(sizeof(int));
free(p);
free(p); // 双重释放
2
3
# 9.5.4 越界访问
- 访问超出分配内存范围的数据。
- 导致程序崩溃或数据损坏。
示例:
int *p = (int *)malloc(5 * sizeof(int));
p[5] = 10; // 越界访问
free(p);
2
3
# 9.5.5 最佳实践
- 初始化指针:在声明指针时初始化为
NULL。 - 检查返回值:在使用
malloc、calloc或realloc后检查返回值是否为NULL。 - 及时释放内存:动态分配的内存使用完毕后及时调用
free。 - 避免野指针:释放内存后将指针置为
NULL。 - 使用工具检测内存问题:如 Valgrind、AddressSanitizer 等。
# 9.6 综合案例实践
# 9.6.1 动态数组
#include <stdio.h>
#include <stdlib.h>
int main() {
int n = 5;
int *arr = (int *)malloc(n * sizeof(int));
if (arr == NULL) {
perror("Failed to allocate memory");
return 1;
}
for (int i = 0; i < n; i++) {
arr[i] = i + 1;
}
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
free(arr);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 9.6.2 二维动态数组
#include <stdio.h>
#include <stdlib.h>
int main() {
int rows = 3, cols = 4;
int **arr = (int **)malloc(rows * sizeof(int *));
if (arr == NULL) {
perror("Failed to allocate memory");
return 1;
}
for (int i = 0; i < rows; i++) {
arr[i] = (int *)malloc(cols * sizeof(int));
if (arr[i] == NULL) {
perror("Failed to allocate memory");
return 1;
}
}
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
arr[i][j] = i * cols + j + 1;
}
}
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%d ", arr[i][j]);
}
printf("\n");
}
for (int i = 0; i < rows; i++) {
free(arr[i]);
}
free(arr);
return 0;
}
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