C++ 数组与指针核心机制解析:从声明语法到静态成员
一、数组声明与初始化:理解核心规则
1.1 声明语法
C++ 中数组声明采用 类型 变量名[大小] 的格式:
int nums[10]; // 包含10个int的数组
double grades[5]; // 包含5个double的数组
为什么不能写成 int[10] nums? 这是 C++ 语法继承自 C 语言的设计——声明由类型说明符和声明符组成,数组维度必须紧跟在变量名后,以明确表明该变量是一个数组对象。这个规则保证了声明的清晰性和一致性。
1.2 内存布局与地址概念
数组元素在内存中连续存储。例如:
int arr[4] = {10, 20, 30, 40};
假设 int 占 4 字节,地址从 0x1000 开始:
| 地址 | 值 |
|---|---|
| 0x1000 | 10 |
| 0x1004 | 20 |
| 0x1008 | 30 |
| 0x100C | 40 |
数组名 arr 的值等同于首元素地址 &arr[0],类型为 int*。但数组名本身是一个**地址常量**,不能修改指向——它不像指针变量那样拥有自己的存储空间。
1.3 初始化方式
int a[5] = {1, 2, 3, 4, 5}; // 完全初始化
int b[5] = {1, 2}; // 部分初始化,剩余元素为0
int c[] = {1, 2, 3}; // 自动推导大小为3
int d[5] = {}; // 所有元素初始化为0
注意:这种花括号初始化只能在定义时使用。定义后无法整体赋值。
1.4 定义后赋值
int a[5];
a = {1, 2, 3, 4, 5}; // 错误!数组名不能作为左值
正确做法是按元素赋值或使用复制函数:
for (int i = 0; i < 5; ++i) a[i] = i + 1;
// 或使用 memcpy
#include <cstring>
int src[5] = {1, 2, 3, 4, 5};
memcpy(a, src, sizeof(a));
二、数组与指针:紧密联系但本质不同
2.1 数组退化
在大多数表达式中,数组名会隐式转换为指向首元素的指针:
int vals[5];
int *ptr = vals; // 等价于 int *ptr = &vals[0];
例外情况:
sizeof(vals)返回整个数组字节数&vals返回指向整个数组的指针,类型为int (*)[5]
2.2 指针遍历数组
int data[5] = {1,2,3,4,5};
int *p = data;
for (int i = 0; i < 5; ++i) {
std::cout << *(p + i) << std::endl; // 等价于 p[i]
}
指针可以重新指向,数组名则不能。
2.3 多维数组的指针
int matrix[2][3];
int (*rowPtr)[3] = matrix; // 指向包含3个int的行
int *elemPtr = matrix[0]; // 指向第一个元素
三、静态成员:类层面的全局数据
3.1 声明与定义
静态数据成员属于类本身而非某个对象,所有实例共享同一份副本。声明方式:
struct Course {
static std::string names[3];
};
静态成员必须在类外提供一次定义,否则会在链接时报错:
std::string Course::names[3]; // 默认初始化为空串
std::string Course::names[3] = {"Math", "Eng", "Sci"}; // 带初始化
为什么不能直接在类内初始化(非 const 整型)? 类的定义通常位于头文件中,若允许多个源文件包含,会引发多重定义链接错误。将定义放在一个源文件中可避免此问题。
3.2 访问方式
std::cout << Course::names[0]; // 推荐通过类名访问
Course c;
std::cout << c.names[0]; // 可行但不推荐
3.3 与全局变量的对比
静态成员受访问控制(public/private)约束,且位于类命名空间内,不易产生命名冲突。全局变量在整个程序可见,容易引发意外重定义错误。
四、动态内存管理:new 与 delete
4.1 栈与堆
- 栈:系统自动管理,分配快但空间有限,存储局部变量和函数参数。
- 堆:手动管理(new/delete),分配慢但灵活,可动态指定大小。
4.2 基本用法
int *p = new int; // 未初始化
int *q = new int(10); // 初始化为10
delete p;
delete q;
int *arr = new int[10]; // 动态数组
delete[] arr; // 必须使用 delete[]
关键规则:
new与delete配对,new[]与delete[]配对- 不能多次释放同一块内存
- 释放后建议将指针置为
nullptr
4.3 动态数组初始化
int *data = new int[5]{1,2,3,4,5}; // C++11 支持列表初始化
4.4 常见错误
- 忘记释放 → 内存泄漏
- 释放后访问 → 悬空指针,未定义行为
- 混用
delete与delete[]→ 内存损坏 - 对栈对象使用
delete→ 程序崩溃
4.5 智能指针(现代 C++)
#include <memory>
std::unique_ptr<int> up = std::make_unique<int>(10);
std::unique_ptr<int[]> arr = std::make_unique<int[]>(10);
智能指针在离开作用域时自动释放内存,降低手动管理风险。
五、现代 C++ 中的改进
5.1 std::array(定长数组)
#include <array>
std::array<int, 5> arr;
arr = {1, 2, 3, 4, 5}; // 整体赋值
std::array 不会退化到指针,支持赋值和容器接口,性能接近原始数组。
5.2 std::vector(动态数组)
#include <vector>
std::vector<int> vec = {1, 2, 3};
vec = {4, 5, 6}; // 整体赋值
vec.push_back(7);
5.3 范围 for 循环
int values[5] = {1,2,3,4,5};
for (int v : values) {
std::cout << v << std::endl;
}
静态数组可用范围 for,而退化为指针的数组不可用。
六、典型错误案例分析
6.1 静态成员未定义
struct S { static int arr[3]; };
int main() { S::arr[0] = 1; } // 链接错误:未定义的引用
解决:添加 int S::arr[3]; 定义。
6.2 数组名赋值
int a[5];
a = {1,2,3,4,5}; // 编译错误
改用 std::array 或逐元素赋值。
6.3 指针与数组混淆
int a[5];
int *p = a;
p = new int[10]; // 允许
a = p; // 错误
6.4 new/delete 不匹配
int *p = new int[10];
delete p; // 错误!应使用 delete[]
6.5 越界访问
int a[5];
a[5] = 10; // 未定义行为
七、最佳实践总结
- 优先使用
std::vector和std::array - 静态成员必须在类外定义一次
- 动态内存优先使用智能指针
- 必要时用引用传递数组,或用
std::array/std::vector - 访问数组前检查下标是否越界
- 理解数组退化行为,需要保留大小时使用模板或
std::size - C 风格字符串优先替换为
std::string