在C++里,数组和指针经常一起出现,但二者在拷贝与赋值上的行为差异很大。很多初学者以为数组可以直接像普通变量一样整体赋值,结果编译器直接报错;而用指针去接数组名,看似成功了,实际只是拿到了地址。下面我们详细拆开讲清楚其中的机制与写法。

数组名的本质与为什么不能整体赋值
在C++中,数组名在大多数表达式里会隐式转换为指向首元素的指针,这被称为“数组到指针的退化”。但数组作为一种复合类型,其类型是包含元素个数信息的,例如int a[5]的类型是int[5]。语言规则明确禁止数组类型的整体赋值和传值拷贝,因此下面这样的代码无法通过编译:
int a[5] = {1, 2, 3, 4, 5};
int b[5];
b = a; // 编译错误:数组不可赋值
上面的b = a之所以错,是因为b作为数组名,在这里并没有退化成指针,而是代表整个数组对象,而数组对象没有可用的赋值运算符。如果写成int* p = a;,这时a退化成int*指针,赋值合法,但只是把首元素地址给了p,并不是拷贝数据。
这种设计的底层原因和历史有关:C语言为了效率不希望数组在传参或赋值时不经意发生大量内存复制,于是规定数组名容易退化为指针;C++沿用了该模型并增加了类型安全检查。理解这一点,就能明白为什么很多“拷贝数组”的错误其实是指针误用。
使用循环进行数组拷贝
最直观也最安全的数组拷贝方式,是用循环逐元素复制。这样不依赖数组退化,也能完整控制目标数组的大小与边界。
#include <iostream>
int main() {
int src[5] = {10, 20, 30, 40, 50};
int dst[5];
// 逐元素拷贝
for (int i = 0; i < 5; ++i) {
dst[i] = src[i];
}
for (int i = 0; i < 5; ++i) {
std::cout << dst[i] << " ";
}
return 0;
}
这种写法优点是非常清晰,任何读者都能一眼看懂在做什么,而且适用于所有元素类型,包括没有默认构造或不可memcpy的类类型。缺点是当数组维度很大或者需要频繁拷贝时,手写循环显得啰嗦。
要注意目标数组必须有足够的空间。如果dst声明为更小的大小,循环越界会导致未定义行为。在实际工程中,常把数组大小用常量或模板参数管理,避免魔法数字。
利用标准库完成拷贝
C++标准库提供了更简洁的算法来完成数组间元素复制。常用的是std::copy,它位于<algorithm>头文件中,利用迭代器语义,对原生数组同样有效。
#include <iostream>
#include <algorithm>
int main() {
int src[5] = {1, 3, 5, 7, 9};
int dst[5];
std::copy(src, src + 5, dst);
for (int i = 0; i < 5; ++i) {
std::cout << dst[i] << " ";
}
return 0;
}
std::copy本质上也是赋值每个元素,但代码更短,并且可以和容器无缝配合。对于平凡类型(trivially copyable)的数组,也可以用C标准库的memcpy做按字节拷贝,效率通常很高。
#include <cstring>
int main() {
int src[5] = {2, 4, 6, 8, 10};
int dst[5];
std::memcpy(dst, src, sizeof(src));
return 0;
}
使用memcpy时必须保证元素类型是平凡可拷贝的,例如内置类型或POD结构;若数组里放的是带有虚函数或自定义构造/析构的类对象,用memcpy会破坏对象语义。因此日常C++更推荐std::copy。
指针赋值与浅拷贝陷阱
当使用指针指向数组时,赋值操作只是复制地址,这叫做浅拷贝。看下面例子:
#include <iostream>
int main() {
int arr[3] = {1, 2, 3};
int* p1 = arr;
int* p2 = p1; // 仅拷贝指针,不拷贝数据
p1[0] = 99;
std::cout << p2[0] << std::endl; // 输出99,因为指向同一数组
return 0;
}
这里p2 = p1让两个指针指向同一块内存,任一方修改都会影响另一方。如果在函数里返回局部数组的指针,或者多个指针管理同一段动态内存,还容易出现重复释放(double free)或悬空指针。
对于动态分配的数组,正确的“拷贝”应该是重新申请内存并复制内容,也就是深拷贝:
#include <cstring>
int main() {
int* src = new int[4]{1, 2, 3, 4};
int* dst = new int[4];
std::memcpy(dst, src, 4 * sizeof(int));
// 使用完后各自释放
delete[] src;
delete[] dst;
return 0;
}
如果只写dst = src;然后delete[] src; delete[] dst;,程序会对同一地址释放两次,直接崩溃。因此指针赋值不等于数组拷贝,管理资源时必须区分清楚。
用std::array和std::vector规避原始数组问题
现代C++更建议使用std::array或std::vector替代原生数组。它们支持整体赋值和拷贝,语义明确。
#include <iostream>
#include <array>
int main() {
std::array<int, 3> a{1, 2, 3};
std::array<int, 3> b = a; // 合法,深拷贝
b[0] = 100;
std::cout << a[0] << " " << b[0] << std::endl; // 1 100
return 0;
}
std::array在栈上分配,大小和原生数组一致,但拥有值语义;std::vector则在堆上动态管理,拷贝时自动深拷贝。用它们就不需要手动处理指针和循环,也避免了数组退化带来的混淆。
当然,在和C接口交互或写底层库时,原生数组和指针仍不可避免。此时牢记:数组名赋值非法,指针赋值只传地址,数据复制请用循环或标准算法。把这些规则内化,数组与指针的拷贝赋值就不再容易出错了。