C/C++ 指针学习笔记

一个类比

  • 张三的快递 : int 张三;
  • 找个快递员 : int *快递员;
  • 张三的货物 : int 货物;
  • 快递员取货 : 快递员 = &货物;
  • 快递员把货给张三 : 张三 = *快递员
1
2
3
4
5
6
7
8
9
10
11
12
13
{
type a;
type *p; // 声明指针

// 因为不同类型的数需要存储的字节数不同,
// 声明指针时必须指定类型。
int *p;
p = &a;
printf(p); // 只会打印整型变量的起始地址

// *p解引用时,计算机知道这是一个指向整型的指针,
// 所以会从起始地址向后读取4个字节(假设int为32位)
}
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
{
int a = 1025;
int *p;
p = &a;
printf("size of integer is %d bytes\n", sizeof(int));
// 使用 %p 格式化C字符串来打印指针地址更规范
printf("Address = %p, value = %d\n", (void*)p, *p);
printf("Address = %p, value = %d\n", (void*)(p + 1), *(p + 1));

// 尝试将整型变量的地址转换为字符指针,char类型只占用一个字节
char *p0;
p0 = (char *)p; // C 风格的强制类型转换

// C++ 中不推荐C风格强转,应使用 reinterpret_cast:
// p0 = reinterpret_cast<char*>(p);

printf("size of char is %d bytes\n", sizeof(char));
printf("Address = %p, value = %d\n", (void*)p0, *p0); // 结果为1 (00000001)
printf("Address = %p, value = %d\n", (void*)(p0 + 1), *(p0 + 1)); // 结果为4 (00000100)

// 1025 (32位int) 在小端序 (Little-Endian) 系统中存储为:
// 00000001 00000100 00000000 00000000
// (低地址) -> (高地址)
// p0 指向最低地址,所以 *p0 是 1
// p0+1 指向第二个字节,所以 *(p0+1) 是 4
}
1
2
3
4
5
6
7
8
9
10
11
12
13
{
int a = 10;
int *p;
p = &a;
printf("Address p is %p\n", (void*)p);
printf("size of integer is %d bytes\n", sizeof(int));
printf("Address p+1 is %p\n", (void*)(p + 1));
/* 假设 p 的地址是 0x6422036 (十六进制)
Address p is 0x6422036
size of integer is 4 bytes
Address p+1 is 0x642203A (地址增加了 4 字节)
*/
}

C++ 引用 (Reference)

C++ 引入了 引用,是一个变量的别名。在功能上类似于指针,但使用上更简洁、更安全。

  • 必须初始化:引用在声明时必须被初始化。
  • 不能为空:没有 “null 引用” 这种东西。
  • 不能重定向:引用一旦绑定到一个变量,就不能再改为引用另一个变量。
1
2
3
4
5
6
7
8
9
int 货物 = 10;
int& 张三 = 货物; // 张三 是 货物 的别名,必须在声明时初始化

张三 = 20; // 相当于 货物 = 20,不需要像指针一样解引用
int* 快递员 = &货物; // 指针版本
*快递员 = 20; // 指针版本

printf("货物: %d\n", 货物); // 输出 20
printf("张三: %d\n", 张三); // 输出 20

引用最强大的用途是在函数参数中(见下文)。

point to pointer (指向指针的指针)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
int x = 5;
int *p = &x;
*p = 6;
int **q = &p;
int ***r = &q;

printf("%p\n", (void*)p); // x 的地址
printf("%d\n", *p); // 6 (x 的值)

printf("%p\n", (void*)q); // p 的地址
printf("%p\n", (void*)*q); // p 存储的地址 (即 x 的地址)
printf("%d\n", **q); // 6 (x 的值)

printf("%p\n", (void*)r); // q 的地址
printf("%p\n", (void*)*r); // q 存储的地址 (即 p 的地址)
printf("%p\n", (void*)**r); // p 存储的地址 (即 x 的地址)
printf("%d\n", ***r); // 6 (x 的值)

***r = 10;
printf("x = %d\n", x); // x 变为 10
}

指针的应用

形参和实参

在 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
// 1. 传值 (Pass-by-Value),函数内修改的是 a 的副本
void Increment_Val(int a)
{
a = a + 1;
}

// 2. 传指针 (Pass-by-Pointer),函数内修改的是 p 指向的内存
void Increment_Ptr(int *p)
{
*p = (*p) + 1;
}

int main()
{
int a = 10;
Increment_Val(a);
printf("a after Val = %d\n", a); // a 仍然是 10

// 你需要传递 a 的地址
Increment_Ptr(&a);
// 或者如果你已经有一个指针:
// int* p = &a;
// Increment_Ptr(p);

printf("a after Ptr = %d\n", a); // a 变为 11
}
// 当a占用的空间很大时 (比如一个大 struct),传指针可以避免昂贵的拷贝

在 C++ 中,我们有了第三种也是更推荐的方式:引用传参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 3. 传引用 (Pass-by-Reference)
// 'a' 在函数内部是实参的别名
void Increment_Ref(int& a)
{
a = a + 1; // 直接修改,无需解引用
}

int main()
{
int a = 10;

// 调用时语法和传值一样简洁
Increment_Ref(a);

printf("a after Ref = %d\n", a); // a 变为 11
}

const 与指针/引用

const 关键字用于防止数据被修改,在 C 和 C++ 中都至关重要。

  1. 常量引用 (Pass-by-Const-Reference)
    这是 C++ 中
    最高效且最安全
    的传递大型只读对象(如 std::string 或大 struct)的方式。避免了拷贝(高效),同时保证函数不会修改原始数据(安全)。

    1
    2
    3
    4
    5
    6
    // s 是一个只读的别名,无法在函数内修改
    void printString(const std::string& s)
    {
    // s = "new string"; // 编译错误!
    printf("%s\n", s.c_str());
    }
  2. const 与指针的两种关系

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    int x = 10, y = 20;

    // 1. 指向常量的指针 (Pointer to Const)
    const int* p1 = &x;
    // *p1 = 15; // 编译错误!不能通过 p1 修改 x
    p1 = &y; // 正确:p1 本身可以被修改,指向其他地址

    // 2. 常量指针 (Const Pointer)
    int* const p2 = &x;
    *p2 = 15; // 正确:可以通过 p2 修改 x
    // p2 = &y; // 编译错误!p2 本身是常量,不能指向其他地址

    // 3. 指向常量的常量指针
    const int* const p3 = &x;
    // *p3 = 15; // 编译错误!
    // p3 = &y; // 编译错误!

数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int A[] = {2, 4, 5, 8, 1}; // 数组有 5 个元素
int i;
int *p = A; // A 自动“退化”为指向第一个元素的指针

// 修正:循环条件应为 i < 5
for (int i = 0; i < 5; i++)
{
printf("Address = %p\n", (void*)&A[i]);
printf("Address = %p\n", (void*)(A + i));
printf("value = %d\n", A[i]);
printf("value = %d\n", *(A + i));
}
// 数组名 A 可以看作是一个特殊的"常量指针"
// A = A + 1; // 编译错误!A 不是一个可修改的左值
p = p + 1; // 正确!p 是一个变量指针

数组作为函数的参数

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
// 数组作为参数时只会传引用(实际上是传递指针)
// 为节省内存,不会拷贝整个数组
// int A[] 和 int* A 在函数参数中是完全等价的

// 错误示范:sizeof(A) 获取的是指针的大小
int SumOfElements_Wrong(int A[])
{
int i, sum = 0;
// 这里的 A 实际上是 int* A
// sizeof(A) 在 64 位系统上是 8 (指针大小)
// sizeof(A[0]) 是 4 (int 大小)
int size = sizeof(A) / sizeof(A[0]); // size 变为 8/4 = 2 (或 4/4 = 1)
printf("SOE_Wrong - Size of A = %d, size of A[0] = %d\n", sizeof(A), sizeof(A[0]));
for (i = 0; i < size; i++) // 循环次数错误
{
sum += A[i];
}
return sum; // 返回错误的结果
}

// 正确示范:必须额外传递数组的大小
int SumOfElements_Correct(int A[], int size)
{
int i, sum = 0;
// sizeof(A) 仍然是指针大小
printf("SOE_Correct - Size of A = %d, size of A[0] = %d\n", sizeof(A), sizeof(A[0]));
for (i = 0; i < size; i++) // 使用传入的 size
{
sum += A[i];
}
return sum;
}

// 因为是传指针,所以可以在函数内修改原始数组
void Double(int* A, int size) // int* A 或 int A[] 都可以
{
int i;
for ( i = 0; i < size; i++)
{
A[i] = 2 * A[i]; // 修改了 main 函数中的原始数组
}
}

int main()
{
int A[] = {1, 2, 3, 4, 5};
int size = sizeof(A) / sizeof(A[0]); // 在这里计算大小 = 20 / 4 = 5
int total = SumOfElements_Correct(A, size);
printf("Total = %d\n", total); // 15
Double(A, size);
// A 现在变为 {2, 4, 6, 8, 10}
}

C++ 现代数组容器

在现代 C++ 中,我们强烈推荐使用标准库容器来代替 C 风格的原始数组。

std::vector (动态数组)

std::vector 是 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
#include <vector>
#include <iostream>

// C++ 中推荐通过 const 引用来传递 vector
int SumOfElements(const std::vector<int>& vec)
{
int sum = 0;
// 1. 使用 .size() 方法获取大小
for (size_t i = 0; i < vec.size(); ++i) {
sum += vec[i];
}

// 2. C++11 引入了更简洁的 "range-based for loop" (范围 for 循环)
sum = 0;
for (int value : vec) {
sum += value;
}
return sum;
}

// 如果需要修改,就传入一个普通的引用
void Double(std::vector<int>& vec)
{
for (int& value : vec) { // 注意这里用 int& (引用)
value = value * 2;
}
}

int main()
{
std::vector<int> V = {1, 2, 3, 4, 5};
V.push_back(6); // 动态添加元素,V 变为 {1, 2, 3, 4, 5, 6}

int total = SumOfElements(V); // 无需手动传递大小
std::cout << "Total = " << total << std::endl; // 21

Double(V);
// V 现在是 {2, 4, 6, 8, 10, 12}
}

std::array (固定大小数组)

如果你需要一个固定大小、在栈上分配(像C数组一样)但又不想让退化为指针的数组,请使用 std::array (C++11)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <array>
#include <iostream>

// 模板参数指定了类型和大小
void PrintArray(const std::array<int, 5>& arr)
{
std::cout << "Size = " << arr.size() << std::endl;
for (int value : arr) {
std::cout << value << " ";
}
}

int main()
{
std::array<int, 5> A = {1, 2, 3, 4, 5};
// A 不会退化为指针,是一个完整的对象
// sizeof(A) = 5 * sizeof(int) = 20
PrintArray(A);
}

字符数组与字符串

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
// NUL 或 '\0' 是一个字符 (值为 0),NULL 是一个宏,通常是 (void*)0
#include<string.h>
#include<stdio.h>

int main()
{
{
// '\0'表示结束,字符数组元素一个个录入时需要显式\0
char C[5] = {'J','O','H','N','\0'};
printf("Size in bytes = %d\n", sizeof(C)); // 5,计入\0
int len = strlen(C);
printf("Length = %d\n", len); // 4,不计入\0
}
{
char C[5] = "JOHN"; // 隐式包含 \0,"JOHN" 实际上是 {'J','O','H','N','\0'}
printf("Size in bytes = %d\n", sizeof(C)); // 5
printf("Length = %d\n", strlen(C)); // 4
}

// 指针与数组的关系
{
char C1[6] = "Hello"; // C1 在栈上,是可修改的
char* C2;
C2 = C1; // C2 指向 C1 的第一个元素
printf("%c\n", C2[1]); // e
C2[0] = 'A'; // 修改 C1 的内容
printf("%s\n", C1); // Aello
// C2[i] 等价于 *(C2+i)
// 反过来 C1 = C2 会发生编译错误,因为 C1 是数组名,不是可修改的左值
}
}

// 数组做函数参数时,虽然写的是 char C[],实际上是 char* C
void print_string(char* C)
{
while (*C != '\0')
{
printf("%c", *C);
C++; // C 是一个指针副本,所以可以修改
}
printf("\n");
}

// 定义一个指针指向字符串字面量
int main_const_string()
{
// "hello" 存储在内存的只读数据区(常量区)
const char* C = "hello"; // C++ 中必须加 const
// char* C = "hello"; // C++ 中已弃用,C 中是合法的

// C[0] = 'A'; // 运行时错误!(段错误) 试图修改只读内存
printf("%s\n", C);
return 0;
}

C++ std::stringstd::string_view

在 C++ 中,你应该总是优先使用 std::string 来处理字符串。

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
#include <string>
#include <iostream>

// 优先使用 const 引用传递
void print_string_cpp(const std::string& s)
{
// s = "new"; // 编译错误 (因为 const)
std::cout << s << std::endl;
std::cout << "Length = " << s.length() << std::endl; // .length() 或 .size()
}

// C++17 引入了 string_view,是一个非拥有的、只读的"视图"
// 可以指向 std::string、char* 或字符串字面量,非常高效
void print_string_view(std::string_view sv)
{
std::cout << sv << std::endl;
}

int main()
{
std::string s1 = "Hello"; // 自动管理内存
std::string s2 = "World";

// 1. 拼接
std::string s3 = s1 + " " + s2; // s3 = "Hello World"

// 2. 修改
s1[0] = 'A'; // s1 = "Aello"
s1.push_back('!'); // s1 = "Aello!"

// 3. 传递
print_string_cpp(s1);

// 4. C++17 string_view
print_string_view(s1); // 从 std::string 创建
print_string_view("I am a C-string"); // 从字面量创建
}

多维数组

一维数组 (回顾)

1
2
3
4
5
6
7
8
{
int A[5] = {2, 4, 6, 8, 10};
int* p = A;
printf("%p\n", (void*)p);
printf("%d\n", *p); // 2
printf("%d\n", *(p + 2)); // 6
// A[i] == *(A+i)
}

二维数组

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
int B[2][3] = { {1,2,3}, {4,5,6} };
// B 是一个包含 2 个元素的数组
// 每个元素是一个 "包含 3 个 int 的数组" (即 int[3])

// B 在“退化”时,返回一个指向 "int[3]" 数组的指针
// 所以 p 的类型必须是 int (*)[3]
int (*p)[3] = B;

// B, &B[0]
// 类型: int (*)[3]
// 值: B[0] 的地址
printf("%p\n", (void*)B);
printf("%p\n", (void*)&B[0]);

// *B, B[0], &B[0][0]
// 类型: int* (退化后)
// 值: B[0][0] 的地址
printf("%p\n", (void*)*B);
printf("%p\n", (void*)B[0]);
printf("%p\n", (void*)&B[0][0]);

// B+1, &B[1]
// 类型: int (*)[3]
// 值: B[1] 的地址 (比 B 的地址多 3 * sizeof(int))
printf("%p\n", (void*)(B + 1));
printf("%p\n", (void*)&B[1]);

// *(B+1), B[1], &B[1][0]
// 类型: int* (退化后)
// 值: B[1][0] 的地址
printf("%p\n", (void*)*(B + 1));
printf("%p\n", (void*)B[1]);
printf("%p\n", (void*)&B[1][0]);

// *(B+1)+2, B[1]+2, &B[1][2]
// 类型: int*
// 值: B[1][2] 的地址
printf("%p\n", (void*)(*(B + 1) + 2));
printf("%p\n", (void*)(B[1] + 2));
printf("%p\n", (void*)&B[1][2]);

// *(*B+1)
// *B 是 B[0] (类型 int*)
// *B+1 是 &B[0][1] (类型 int*)
// *(*B+1) 是 B[0][1] (类型 int),值是 2
printf("%d\n", *(*B + 1)); // B[0][1]

// 访问 B[i][j]
B[i][j] == *(B[i] + j) == *(*(B + i) + j)

三维数组

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
int C[3][2][2];
// C 是一个 "包含 3 个元素的数组"
// 每个元素是一个 "包含 2 个元素的数组" (即 T[2])
// 每个元素 (T) 是一个 "包含 2 个 int 的数组" (即 int[2])
// 所以 C 的元素类型是 int[2][2]

// C 退化为指向 int[2][2] 数组的指针
int (*p)[2][2] = C;

// C, &C[0]
// 类型: int (*)[2][2]
// 值: C[0] 的地址
printf("%p\n", (void*)C);

// *C, C[0], &C[0][0]
// 类型: int (*)[2] (指向 C[0][0] 的指针)
// 值: C[0][0] 的地址
printf("%p\n", (void*)*C);

// C[i][j][k] == *(C[i][j] + k) == *(*(C[i] + j) + k) == *(*(*(C + i) + j) + k)

// 作为函数参数,第一个维度可以省略,其他必须指定
void Func(int A[][2][2])
{
// ...
}

C++ 多维数组的替代品

  1. std::array 嵌套std::array<std::array<int, 3>, 2> B;
  2. std::vector 嵌套std::vector<std::vector<int>> B;
    • 优点:易于使用,可以是不规则数组(每行长度不同)。
    • 缺点:数据在内存中不是连续存储的,可能导致缓存未命中,性能较差。
  3. 使用 std::vector 模拟
    对于性能敏感的 2D 矩阵,通常使用一个**一维 vector**,并通过数学计算来模拟 2D 索引。
    1
    2
    3
    4
    int rows = 2, cols = 3;
    std::vector<int> B(rows * cols); // 分配 6 个元素的空间
    // 访问 B[i][j] 变为:
    B[i * cols + j] = 5; // 例如 B[1][1] -> B[1 * 3 + 1] = B[4]

指针和动态内存

  • **栈区 (Stack)**:用于存放函数参数、局部变量。由编译器自动管理(函数开始时分配,结束时释放)。空间有限,速度快。
  • **堆区 (Heap)**:用于存放程序运行时动态分配的内存。由程序员手动管理。空间大,分配/释放较慢。

C 风格动态内存 (malloc, free)

C 使用 stdlib.h 中的 malloc, calloc, realloc, free

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
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p;

// malloc: 分配指定字节的内存。
// 返回 void* 指针,需要强转。内存内容是未初始化的 (垃圾值)。
p = (int*)malloc(sizeof(int));
if (p == NULL) { // 必须检查分配是否成功
printf("Memory allocation failed!\n");
return 1;
}
*p = 10;

// 必须手动释放,否则造成内存泄漏
free(p);
p = NULL; // 释放后最好将指针设为 NULL,防止"悬垂指针"

// calloc: (count, size) 分配 count * size 字节,并将内存初始化为 0
int* A = (int*)calloc(20, sizeof(int)); // 分配 20 个 int 的数组,全为 0

// realloc: 调整已分配内存的大小
int* B = (int*)realloc(A, 25 * sizeof(int)); // 扩展到 25 个 int
if (B != NULL) {
A = B; // realloc 可能会移动内存,所以要更新指针
}

free(A); // 释放 realloc 后的指针
}

C++ 动态内存 (new, delete)

C++ 使用 newdelete 操作符来取代 mallocfree。更安全,因为会自动调用构造函数和析构函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <string>

// 1. 单个对象
int* p = new int; // 分配一个 int,调用 int 的默认构造
*p = 10;
delete p; // 释放内存

std::string* s = new std::string("Hello"); // 分配并调用构造函数
delete s; // 释放内存,并自动调用 std::string 的析构函数

// 2. 数组
int* A = new int[20]; // 分配 20 个 int 的数组
// ...
delete[] A; // 释放数组必须使用 delete[]!

// ----------------------------------------------------
// !!! 常见的致命错误 !!!
// 1. new[] 必须配对 delete[]
// 2. new 必须配对 delete
// 3. malloc 必须配对 free
// ----------------------------------------------------
// delete A; // 错误!只释放了第一个元素,导致 19 个 int 泄漏
// free(p); // 错误!未定义行为
// delete p; p = NULL; // C++ 中也推荐设为 NULL

C++ 核心:RAII 和智能指针 (Smart Pointers)

手动管理 newdelete 仍然很容易出错(忘记 delete、异常导致 delete 未被执行等)。
现代 C++ 提倡 RAII (Resource Acquisition Is Initialization) 思想,即使用对象来管理资源。当对象离开作用域时,其析构函数会自动释放资源。

智能指针就是 RAII 的完美实践。在 <memory> 头文件中。

1. std::unique_ptr (C++11) - 独占所有权

unique_ptr 表示独占所指向的对象。不能被复制,只能被移动 (Move)。这是 new 的首选替代品。

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
#include <memory>
#include <iostream>

void UseMyPointer(std::unique_ptr<int> ptr)
{
std::cout << "Using pointer, value: " << *ptr << std::endl;
} // 'ptr' 在这里离开作用域,自动 'delete' 所管理的内存

int main()
{
// 1. 创建
std::unique_ptr<int> p1 = std::make_unique<int>(100); // 推荐方式
// std::unique_ptr<int> p1(new int(100)); // 也可以

// 2. 使用
*p1 = 200;

// 3. 转移所有权 (Move)
// p2 = p1; // 编译错误!unique_ptr 不能复制
std::unique_ptr<int> p2 = std::move(p1); // p1 变为 'nullptr', p2 获得所有权

// p1 现在是空的 (nullptr)

// 4. 传递给函数 (所有权也转移了)
UseMyPointer(std::move(p2));

// p2 现在也是空的

// 数组版本
std::unique_ptr<int[]> p_arr = std::make_unique<int[]>(50);
p_arr[0] = 10;
// 当 p_arr 离开作用域时,会自动调用 'delete[]'

return 0; // 所有内存自动释放,无泄漏!
}

2. std::shared_ptr (C++11) - 共享所有权

shared_ptr 使用引用计数来管理对象。当最后一个 shared_ptr 被销毁时,对象才会被 delete

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
#include <memory>
#include <vector>

std::shared_ptr<int> p_global; // 一个全局共享指针

void Observe()
{
p_global = std::make_shared<int>(42);
// 引用计数 = 1
std::cout << "Use count: " << p_global.use_count() << std::endl;
}

int main()
{
Observe();
// 引用计数 = 1 (p_global)

std::shared_ptr<int> p_local = p_global; // 复制,引用计数增加
// 引用计数 = 2 (p_global 和 p_local)
std::cout << "Use count: " << p_global.use_count() << std::endl;

{
std::shared_ptr<int> p_inner = p_local; // 复制
// 引用计数 = 3
std::cout << "Use count: " << p_global.use_count() << std::endl;
} // p_inner 销毁,引用计数 = 2

// p_global = nullptr; // p_global 释放所有权,引用计数 = 1

return 0; // main 结束, p_local 销毁, 引用计数 = 1
// 程序结束, p_global 销毁, 引用计数 = 0, 内存被 'delete'
}

函数返回指针

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
#include<stdio.h>
#include<stdlib.h>

void PrintHelloWorld()
{
printf("Hello World\n");
}

// 严重错误!
int* Add(int* a, int* b)
{
int c = (*a) + (*b);
return &c; // 'c' 是局部变量,存储在栈上
} // Add 函数返回时,'c' 的内存被释放!

int main_bad()
{
int a = 2, b = 4;
int* ptr = Add(&a, &b); // ptr 指向一个已释放的栈内存(悬垂指针)

PrintHelloWorld(); // 这个函数调用会覆盖掉原来 'c' 所在的栈内存

// 打印 *ptr 是未定义行为!可能崩溃,也可能打印垃圾值
printf("Sum = %d\n", *ptr);
return 0;
}

正确的做法

  1. 调用者传入用于存储结果的指针 (Pass-by-Pointer for output)。
  2. 函数返回在上分配的内存(调用者必须负责 free)。
  3. (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
// 做法 2:返回堆内存
int* Add_Heap(int* a, int* b)
{
int* c = (int*)malloc(sizeof(int)); // 在堆上分配
*c = (*a) + (*b);
return c; // 返回堆内存地址是安全的
}

// 做法 3 (C++):返回智能指针
std::unique_ptr<int> Add_Smart(int a, int b) {
// 使用 make_unique 自动在堆上创建
return std::make_unique<int>(a + b);
}

int main_ok()
{
int a = 2, b = 4;

// C 方式
int* ptr_c = Add_Heap(&a, &b);
printf("Sum (C) = %d\n", *ptr_c);
free(ptr_c); // 调用者必须手动释放!

// C++ 方式
std::unique_ptr<int> ptr_cpp = Add_Smart(a, b);
printf("Sum (C++) = %d\n", *ptr_cpp);
// 无需释放,ptr_cpp 离开作用域时自动释放内存

return 0;
}

函数指针

函数指针存放了函数在内存中的入口地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<stdio.h> 
int Add(int a, int b)
{
return a + b;
}
int main()
{
int c;

// 声明一个函数指针 p
// p 指向一个 "返回 int,参数为 (int, int)" 的函数
int (*p)(int, int);

p = &Add; // & 是可选的,p = Add; 也可以

c = (*p)(2, 3); // * 是可选的,c = p(2, 3); 也可以

printf("%d\n", c); // 5
}

函数指针的应用 (回调)

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
#include<stdio.h>
// 比较函数,用于qsort或自定义排序
int compare(int a, int b)
{
if (a > b) return 1;
else return -1;
}

// BubbleSort 接受一个函数指针作为参数
void BubbleSort(int *A, int n, int (*compare_func)(int, int))
{
int i, j, temp;
for (i = 0; i < n; i++)
for (j = 0; j < n - 1; j++) {
// 使用传入的函数指针进行比较
if (compare_func(A[j], A[j + 1]) > 0) {
temp = A[j];
A[j] = A[j + 1];
A[j + 1] = temp;
}
}
}
int main_sort()
{
int i, A[] = {3, 2, 1, 5, 4, 6};
BubbleSort(A, 6, compare); // 传递函数名,会自动退化为函数指针
for(i = 0; i < 6; i++) printf("%d ", A[i]); // 1 2 3 4 5 6
return 0;
}

C++ 可调用对象 (Callables)

在 C++ 中,函数指针的概念被扩展为**可调用对象 (Callable)**。包括:

  1. 函数指针 (C 风格)
  2. 函数对象 (Functor):重载了 operator() 的类/结构体。
  3. Lambda 表达式 (C++11):匿名的、内联的函数。

1. Lambda 表达式 (C++11)

这是 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
#include <algorithm> // C++ 标准库排序
#include <vector>

int main()
{
std::vector<int> V = {3, 2, 1, 5, 4, 6};

// 使用 Lambda 表达式代替 compare 函数
// [] 表示不捕获任何外部变量
// (int a, int b) 是参数
// { ... } 是函数体
std::sort(V.begin(), V.end(), [](int a, int b) {
return a < b; // 升序排序
});

// 结果: {1, 2, 3, 4, 5, 6}

// 捕获外部变量
int offset = 10;
// [offset] 表示以传值方式捕获 'offset'
std::for_each(V.begin(), V.end(), [offset](int& val) {
val += offset; // val = val + 10
});
// V 现在是 {11, 12, 13, 14, 15, 16}
}

2. std::function (C++11)

如果你需要一个通用的”容器”来存储任何类型的可调用对象(函数指针、Lambda、Functor),请使用 std::function (在 <functional> 头文件中)。

是 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
#include <functional>
#include <vector>
#include <iostream>

// BubbleSort 现在接受一个 std::function
void BubbleSort_CPP(std::vector<int>& A, std::function<bool(int, int)> compare_func)
{
int n = A.size();
for (int i = 0; i < n; i++)
for (int j = 0; j < n - 1; j++) {
if (compare_func(A[j], A[j + 1])) { // 直接调用
std::swap(A[j], A[j + 1]);
}
}
}

// C 风格函数
bool GreaterThan(int a, int b) { return a > b; }

int main()
{
std::vector<int> V = {3, 2, 1, 5, 4, 6};

// 1. 传递一个 Lambda
BubbleSort_CPP(V, [](int a, int b) {
return a > b; // 降序
});
// V = {6, 5, 4, 3, 2, 1}

// 2. 传递一个 C 风格函数
BubbleSort_CPP(V, GreaterThan); // 仍然是降序
}