第9章 数组和指针

数组

数组的基本概念

数组是一种数据结构,用于存储相同类型的多个元素。在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
#include <iostream>

int main() {
// 声明一个包含5个整数的数组
int numbers[5];

// 初始化数组元素
numbers[0] = 10;
numbers[1] = 20;
numbers[2] = 30;
numbers[3] = 40;
numbers[4] = 50;

// 声明并初始化数组
int scores[3] = {95, 87, 91}; // 初始化前3个元素,其余为0

// 声明并初始化所有元素
double prices[] = {19.99, 29.99, 39.99, 49.99}; // 编译器自动计算大小

// 字符数组(字符串)
char message[] = "Hello, World!";

// 遍历数组
std::cout << "Numbers: ";
for (int i = 0; i < 5; i++) {
std::cout << numbers[i] << " ";
}
std::cout << std::endl;

// 使用范围for循环(C++11+)
std::cout << "Scores: ";
for (int score : scores) {
std::cout << score << " ";
}
std::cout << std::endl;

return 0;
}

数组的特性

  • 连续存储:数组元素在内存中是连续存储的
  • 固定大小:数组大小在声明时确定,不能动态改变
  • 索引访问:使用下标(从0开始)访问数组元素
  • 边界检查: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
#include <iostream>

int main() {
// 声明一个2x3的二维数组
int matrix[2][3] = {
{1, 2, 3},
{4, 5, 6}
};

// 访问二维数组元素
std::cout << "matrix[0][0]: " << matrix[0][0] << std::endl;
std::cout << "matrix[1][2]: " << matrix[1][2] << std::endl;

// 遍历二维数组
std::cout << "Matrix: " << std::endl;
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
std::cout << matrix[i][j] << " ";
}
std::cout << std::endl;
}

// 三维数组
int cube[2][2][2] = {
{{1, 2}, {3, 4}},
{{5, 6}, {7, 8}}
};

return 0;
}

指针

指针的基本概念

指针是一种变量,用于存储内存地址。通过指针,可以间接访问和修改内存中的数据。

指针的声明和初始化

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

int main() {
// 声明一个整数变量
int x = 100;

// 声明一个指向整数的指针,并初始化为x的地址
int* ptr = &x;

// 输出变量的值和地址
std::cout << "x = " << x << std::endl;
std::cout << "&x = " << &x << std::endl;
std::cout << "ptr = " << ptr << std::endl;
std::cout << "*ptr = " << *ptr << std::endl; // 解引用指针,获取值

// 通过指针修改变量的值
*ptr = 200;
std::cout << "After modification: " << std::endl;
std::cout << "x = " << x << std::endl;
std::cout << "*ptr = " << *ptr << std::endl;

// 空指针
int* nullPtr = nullptr; // C++11+推荐使用nullptr
int* oldNullPtr = NULL; // 传统方式

return 0;
}

指针的运算

指针算术是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
#include <iostream>

int main() {
int numbers[] = {10, 20, 30, 40, 50};
int* ptr = numbers; // 指向数组第一个元素

// 指针算术运算
std::cout << "*ptr = " << *ptr << std::endl; // 10
ptr++; // 指针向后移动一个元素(移动sizeof(int)字节)
std::cout << "*ptr = " << *ptr << std::endl; // 20
ptr += 2; // 指针向后移动两个元素
std::cout << "*ptr = " << *ptr << std::endl; // 40
ptr--; // 指针向前移动一个元素
std::cout << "*ptr = " << *ptr << std::endl; // 30

// 指针比较
int* start = numbers;
int* end = numbers + 5; // 指向数组末尾的下一个位置

std::cout << "Array elements: ";
while (start < end) {
std::cout << *start << " ";
start++;
}
std::cout << std::endl;

return 0;
}

指针算术的规则

  1. 指针与整数的加减

    • ptr + n:指针向后移动n个元素,实际移动的字节数为 n * sizeof(*ptr)
    • ptr - n:指针向前移动n个元素,实际移动的字节数为 n * sizeof(*ptr)
    • ptr++/++ptr:指针向后移动1个元素
    • ptr--/--ptr:指针向前移动1个元素
  2. 指针之间的减法

    • ptr2 - ptr1:计算两个指针之间的元素个数,结果为整数类型(ptrdiff_t)
    • 要求两个指针指向同一个数组或同一个对象的不同部分
  3. 指针比较运算

    • ==!=:判断两个指针是否指向同一位置
    • <<=>>=:判断指针在内存中的相对位置
    • 要求两个指针指向同一个数组或同一个对象的不同部分

指针算术的应用场景

  1. 数组遍历:使用指针算术遍历数组元素
  2. 动态内存管理:在动态分配的内存块中导航
  3. 字符串处理:操作C风格字符串
  4. 数据结构实现:实现链表、树等数据结构
  5. 性能优化:指针算术通常比数组下标访问更快

指针算术的注意事项

  1. 越界访问:指针算术可能导致越界访问,应确保指针始终在有效范围内
  2. 类型安全:指针算术依赖于指针的类型,不同类型的指针算术结果不同
  3. 空指针:对空指针进行算术运算会导致未定义行为
  4. 野指针:对野指针进行算术运算会导致未定义行为
  5. void指针:void指针不支持算术运算,因为它的大小未知

指针算术示例

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 <iostream>

int main() {
double values[] = {1.1, 2.2, 3.3, 4.4, 5.5};
double* p = values;

// 指针与整数相加
std::cout << "p = " << p << ", *p = " << *p << std::endl;
p += 2; // 移动2个double元素,即16字节(假设double为8字节)
std::cout << "p += 2: " << p << ", *p = " << *p << std::endl;

// 指针之间的减法
double* start = values;
double* end = values + 5;
ptrdiff_t count = end - start;
std::cout << "Elements count: " << count << std::endl;

// 指针比较
if (start < end) {
std::cout << "start is before end" << std::endl;
}

return 0;
}

指针与数组的关系

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

int main() {
int numbers[] = {10, 20, 30, 40, 50};

// 数组名是指向第一个元素的常量指针
std::cout << "*numbers = " << *numbers << std::endl; // 10
std::cout << "*(numbers + 1) = " << *(numbers + 1) << std::endl; // 20

// 指针可以像数组一样使用下标
int* ptr = numbers;
std::cout << "ptr[0] = " << ptr[0] << std::endl; // 10
std::cout << "ptr[2] = " << ptr[2] << std::endl; // 30

// 数组可以像指针一样进行算术运算
std::cout << "*(numbers + 3) = " << *(numbers + 3) << std::endl; // 40

return 0;
}

指针与函数

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 <iostream>

// 指针作为函数参数(传地址)
void increment(int* value) {
(*value)++;
}

// 函数返回指针
int* createArray(int size) {
int* arr = new int[size];
for (int i = 0; i < size; i++) {
arr[i] = i * 10;
}
return arr;
}

int main() {
int x = 5;
std::cout << "Before: x = " << x << std::endl;
increment(&x);
std::cout << "After: x = " << x << std::endl;

// 使用函数返回的指针
int* numbers = createArray(5);
std::cout << "Array elements: ";
for (int i = 0; i < 5; i++) {
std::cout << numbers[i] << " ";
}
std::cout << std::endl;

// 释放动态分配的内存
delete[] numbers;

return 0;
}

指向指针的指针

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

int main() {
int x = 100;
int* ptr = &x;
int** ptrToPtr = &ptr;

std::cout << "x = " << x << std::endl;
std::cout << "*ptr = " << *ptr << std::endl;
std::cout << "**ptrToPtr = " << **ptrToPtr << std::endl;

// 通过指向指针的指针修改值
**ptrToPtr = 200;
std::cout << "After modification: " << std::endl;
std::cout << "x = " << x << std::endl;
std::cout << "*ptr = " << *ptr << std::endl;
std::cout << "**ptrToPtr = " << **ptrToPtr << std::endl;

return 0;
}

动态内存分配

new 和 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <iostream>

int main() {
// 动态分配单个整数
int* pInt = new int;
*pInt = 42;
std::cout << "*pInt = " << *pInt << std::endl;
delete pInt; // 释放内存

// 动态分配数组
int size = 5;
int* pArray = new int[size];

// 初始化数组
for (int i = 0; i < size; i++) {
pArray[i] = i * 10;
}

// 输出数组元素
std::cout << "Array elements: ";
for (int i = 0; i < size; i++) {
std::cout << pArray[i] << " ";
}
std::cout << std::endl;

delete[] pArray; // 释放数组内存

// 动态分配对象
class Person {
public:
std::string name;
int age;

Person(std::string n, int a) : name(n), age(a) {
std::cout << "Person constructor called" << std::endl;
}

~Person() {
std::cout << "Person destructor called" << std::endl;
}

void display() {
std::cout << "Name: " << name << ", Age: " << age << std::endl;
}
};

Person* pPerson = new Person("Alice", 30);
pPerson->display();
delete pPerson;

return 0;
}

智能指针

智能指针是C++11引入的一种RAII(资源获取即初始化)机制,用于自动管理动态内存,避免内存泄漏。智能指针本质上是一个包装了裸指针的类,通过析构函数自动释放所管理的内存。

智能指针的种类

  1. std::unique_ptr:独占所有权的智能指针
  2. std::shared_ptr:共享所有权的智能指针
  3. std::weak_ptr:不增加引用计数的shared_ptr观察者

std::unique_ptr

std::unique_ptr是一种独占所有权的智能指针,同一时间只能有一个unique_ptr指向同一个对象:

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

class Person {
public:
std::string name;
int age;

Person(std::string n, int a) : name(n), age(a) {
std::cout << "Person constructor called for " << name << std::endl;
}

~Person() {
std::cout << "Person destructor called for " << name << std::endl;
}

void display() {
std::cout << "Name: " << name << ", Age: " << age << std::endl;
}
};

int main() {
// 方法1:使用new初始化
std::unique_ptr<Person> p1(new Person("Alice", 30));
p1->display();

// 方法2:使用make_unique(C++14+,推荐)
auto p2 = std::make_unique<Person>("Bob", 25);
p2->display();

// 所有权转移
std::unique_ptr<Person> p3 = std::move(p1); // p1现在为空
if (p1) {
p1->display();
} else {
std::cout << "p1 is empty" << std::endl;
}
p3->display();

// 数组版本
auto pArray = std::make_unique<Person[]>(3);

return 0; // 智能指针自动释放内存
}

std::shared_ptr

std::shared_ptr是一种共享所有权的智能指针,多个shared_ptr可以指向同一个对象,通过引用计数管理对象的生命周期:

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

int main() {
// 方法1:使用new初始化
std::shared_ptr<Person> p1(new Person("Charlie", 35));
std::cout << "Reference count: " << p1.use_count() << std::endl;

// 方法2:使用make_shared(推荐,更高效)
auto p2 = std::make_shared<Person>("David", 40);
std::cout << "Reference count: " << p2.use_count() << std::endl;

// 共享所有权
std::shared_ptr<Person> p3 = p2;
std::cout << "Reference count after copy: " << p2.use_count() << std::endl;

// 所有权转移
std::shared_ptr<Person> p4 = std::move(p1);
std::cout << "p1 use_count: " << p1.use_count() << std::endl;
std::cout << "p4 use_count: " << p4.use_count() << std::endl;

return 0; // 当引用计数为0时,对象自动释放
}

std::weak_ptr

std::weak_ptr是一种不增加引用计数的智能指针,用于观察shared_ptr管理的对象,避免循环引用:

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

class Node {
public:
int value;
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // 使用weak_ptr避免循环引用

Node(int v) : value(v) {
std::cout << "Node constructor called for " << value << std::endl;
}

~Node() {
std::cout << "Node destructor called for " << value << std::endl;
}
};

int main() {
auto n1 = std::make_shared<Node>(1);
auto n2 = std::make_shared<Node>(2);

n1->next = n2;
n2->prev = n1;

// 使用weak_ptr
std::weak_ptr<Node> wp = n1;

// 检查对象是否仍然存在
if (auto sp = wp.lock()) {
std::cout << "Node value: " << sp->value << std::endl;
} else {
std::cout << "Node no longer exists" << std::endl;
}

// 检查引用计数
std::cout << "n1 use_count: " << n1.use_count() << std::endl;
std::cout << "n2 use_count: " << n2.use_count() << std::endl;

return 0;
}

智能指针的原理

  1. RAII机制:智能指针在构造时获取资源,在析构时释放资源
  2. 所有权管理
    • unique_ptr:独占所有权,不允许复制,只允许移动
    • shared_ptr:共享所有权,通过引用计数追踪所有者数量
    • weak_ptr:不拥有所有权,只观察shared_ptr管理的对象
  3. 析构函数:智能指针的析构函数自动调用delete或delete[]释放内存

智能指针的使用场景

  1. std::unique_ptr

    • 独占资源的场景
    • 作为函数返回值
    • 作为类成员变量
    • 管理局部动态对象
  2. std::shared_ptr

    • 多个所有者共享资源的场景
    • 跨作用域共享对象
    • 复杂数据结构(如链表、树)中的节点
    • 长时间存在的对象
  3. std::weak_ptr

    • 打破shared_ptr的循环引用
    • 观察对象而不延长其生命周期
    • 缓存场景

智能指针的最佳实践

  1. 优先使用make_unique和make_shared

    • 更安全:避免内存泄漏(如果在构造过程中抛出异常)
    • 更高效:make_shared只分配一次内存(对象和控制块)
  2. 选择合适的智能指针

    • 默认使用unique_ptr,当需要共享所有权时使用shared_ptr
    • 避免不必要的shared_ptr,因为引用计数有开销
  3. 避免裸指针与智能指针混用

    • 不要用智能指针管理已经由裸指针管理的内存
    • 不要从智能指针获取裸指针后手动释放
  4. 注意循环引用

    • 在双向引用的场景中,使用weak_ptr打破循环
  5. 正确处理数组

    • unique_ptr支持数组版本:std::unique_ptr<T[]>
    • shared_ptr需要自定义删除器来管理数组
  6. 传递智能指针

    • 对于unique_ptr,使用移动语义传递
    • 对于shared_ptr,根据需要传递值或引用

智能指针的性能考量

  1. 内存开销

    • unique_ptr:几乎无额外开销,与裸指针大小相同
    • shared_ptr:有额外开销(控制块、引用计数)
    • weak_ptr:与shared_ptr类似,需要访问控制块
  2. 时间开销

    • unique_ptr:操作几乎无开销
    • shared_ptr:引用计数的增减需要原子操作,有一定开销
    • make_shared:比直接使用new更高效
  3. 优化策略

    • 优先使用unique_ptr
    • 合理使用make_shared减少内存分配
    • 避免频繁的shared_ptr复制
    • 注意移动语义的使用

数组和指针的安全性

常见错误

  1. 空指针解引用:尝试访问空指针指向的内存
  2. 野指针:指针指向已经释放的内存
  3. 内存泄漏:动态分配的内存没有释放
  4. 数组越界:访问超出数组范围的元素
  5. 悬挂引用:引用指向已经销毁的对象

安全实践

  1. 初始化指针:总是将指针初始化为nullptr或有效的内存地址
  2. 检查空指针:在使用指针前检查是否为nullptr
  3. 使用智能指针:优先使用unique_ptr和shared_ptr管理动态内存
  4. 避免裸指针:尽量减少使用裸指针,尤其是在管理动态内存时
  5. 使用std::array:对于固定大小的数组,使用std::array替代内置数组
  6. 使用std::vector:对于可变大小的数组,使用std::vector替代内置数组
  7. 边界检查:在访问数组元素时确保不越界
  8. 使用RAII:使用资源获取即初始化的原则管理资源

数组和指针的应用

字符串处理

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

int main() {
// C风格字符串(字符数组)
char str1[] = "Hello";
char str2[20];

// 字符串复制
std::strcpy(str2, str1);
std::cout << "str2: " << str2 << std::endl;

// 字符串连接
std::strcat(str2, " World");
std::cout << "str2 after concatenation: " << str2 << std::endl;

// 字符串长度
std::cout << "Length of str2: " << std::strlen(str2) << std::endl;

// 字符串比较
char str3[] = "Hello";
char str4[] = "World";

int result = std::strcmp(str3, str4);
if (result < 0) {
std::cout << str3 << " is less than " << str4 << std::endl;
} else if (result > 0) {
std::cout << str3 << " is greater than " << str4 << std::endl;
} else {
std::cout << str3 << " is equal to " << str4 << std::endl;
}

return 0;
}

函数参数

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

// 数组作为函数参数(退化为指针)
void printArray(int arr[], int size) {
for (int i = 0; i < size; i++) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
}

// 使用指针接收数组
void modifyArray(int* arr, int size) {
for (int i = 0; i < size; i++) {
arr[i] *= 2;
}
}

// 使用引用接收数组(保持数组类型信息)
template <size_t N>
void printArrayWithReference(int (&arr)[N]) {
for (int i = 0; i < N; i++) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
}

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

std::cout << "Original array: ";
printArray(numbers, size);

modifyArray(numbers, size);
std::cout << "Modified array: ";
printArray(numbers, size);

std::cout << "Using reference: ";
printArrayWithReference(numbers);

return 0;
}

多维数组和指针

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

// 二维数组作为函数参数
void print2DArray(int arr[][3], int rows) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 3; j++) {
std::cout << arr[i][j] << " ";
}
std::cout << std::endl;
}
}

// 使用指针的指针
void printUsingDoublePointer(int** arr, int rows, int cols) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
std::cout << arr[i][j] << " ";
}
std::cout << std::endl;
}
}

int main() {
// 静态二维数组
int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}};
std::cout << "Static 2D array: " << std::endl;
print2DArray(matrix, 2);

// 动态二维数组(使用指针的指针)
int rows = 2;
int cols = 3;

// 分配行指针
int** dynamicMatrix = new int*[rows];

// 分配每一行
for (int i = 0; i < rows; i++) {
dynamicMatrix[i] = new int[cols];
}

// 初始化
int value = 1;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
dynamicMatrix[i][j] = value++;
}
}

std::cout << "Dynamic 2D array: " << std::endl;
printUsingDoublePointer(dynamicMatrix, rows, cols);

// 释放内存
for (int i = 0; i < rows; i++) {
delete[] dynamicMatrix[i];
}
delete[] dynamicMatrix;

return 0;
}

总结

数组和指针是C++中非常重要的概念,它们密切相关且功能强大。通过本章的学习,你应该掌握了:

  1. 数组的基本概念:声明、初始化、访问和遍历
  2. 指针的基本概念:声明、初始化、解引用和算术运算
  3. 数组和指针的关系:数组名作为指针、指针作为数组使用
  4. 动态内存分配:使用new和delete管理动态内存
  5. 智能指针:使用unique_ptr和shared_ptr自动管理内存
  6. 安全性:避免常见的指针和数组错误
  7. 应用:字符串处理、函数参数、多维数组等

数组和指针是C++的基础,也是理解更高级特性(如引用、模板、STL等)的关键。通过不断练习和实践,你会逐渐掌握它们的使用技巧,并能够编写更高效、更安全的代码。