指针、引用、结构体及内存管理
1. 引用 (Reference) 和指针 (Pointer)
这是C++引入"引用"后最经典的对比。总的来说,引用是变量的"别名",而指针是存储变量地址的"变量"。
指针 (Pointer)
-
概念: 指针是一个变量,它本身有自己的内存地址,但它存储的内容是另一个变量的内存地址。通过这个地址,我们可以间接地访问或修改那个变量。
-
核心特点:
- 可为空 (Nullable) : 指针可以被设置为
nullptr
(或在旧代码中是NULL
),表示它不指向任何有效的内存地址。 - 可重新赋值 (Re-seatable): 指针可以在其生命周期内改变指向,让它先后指向不同的变量。
- 需要解引用 (Dereferencing) : 访问指针所指向的数据时,需要使用解引用操作符
*
。 - 拥有自己的内存: 指针变量本身占用内存空间(通常是4或8字节,取决于系统架构)。
- 支持指针算术: 可以对指针进行加减运算,使其在内存中前后移动,这对于数组操作非常重要。
- 可为空 (Nullable) : 指针可以被设置为
-
用法示例:
cppint a = 10; int b = 20; int* ptr; // 声明一个整型指针 ptr = &a; // 1. 指向变量 a 的地址 cout << *ptr << endl; // 2. 解引用,输出 a 的值:10 *ptr = 15; // 3. 通过指针修改 a 的值,现在 a = 15 ptr = &b; // 4. 重新赋值,使其指向 b cout << *ptr << endl; // 输出 b 的值:20 ptr = nullptr; // 5. 设置为空指针
引用 (Reference)
-
概念: 引用可以看作是一个已经存在的变量的别名。它不是一个新变量,不占用新的内存空间(或者说,它与原变量共享内存地址)。它提供了一种更简洁、更安全的方式来操作变量。
-
核心特点:
- 不能为空 (Not Nullable) : 引用必须在声明时进行初始化,并且不能指向
nullptr
。它必须始终关联一个有效的变量。 - 不可重新赋值 (Not Re-seatable): 一旦引用被初始化为某个变量的别名,它就不能再被改变为另一个变量的别名。对引用的赋值操作会直接修改它所引用的原始变量的值。
- 无需解引用: 使用引用时,语法上就像直接使用原始变量一样,不需要特殊的操作符。
- 不拥有自己的内存: 它只是一个别名,与原变量共享同一块内存。
- 不支持算术: 不能对引用进行算术运算。
- 不能为空 (Not Nullable) : 引用必须在声明时进行初始化,并且不能指向
-
用法示例:
cppint a = 10; int b = 20; int& ref = a; // 1. 声明并初始化引用,ref 成为 a 的别名。必须在声明时初始化! // int& ref2; // 错误!引用必须初始化。 cout << ref << endl; // 2. 直接使用,输出 a 的值:10 ref = 15; // 3. 修改 ref 就是修改 a,现在 a = 15 ref = b; // 4. 注意!这里不是让 ref 成为 b 的别名! // 而是将 b 的值 (20) 赋给 ref 所引用的变量 a。 // 执行后,a 的值变为 20,ref 仍然是 a 的别名。 cout << "a = " << a << endl; // 输出 a = 20 cout << "ref = " << ref << endl; // 输出 ref = 20
总结
特性 | 指针 (Pointer) | 引用 (Reference) |
---|---|---|
本质 | 存储地址的变量 | 变量的别名 |
空值 | 可以为 nullptr |
必须引用一个已存在的变量,不能为空 |
初始化 | 可以在任何时候初始化 | 必须在声明时初始化 |
重定向 | 可以改变指向,指向另一个变量 | 一旦初始化,终生指向同一个变量 |
内存空间 | 自身占用内存空间 | 不占用独立内存空间(与原变量共享) |
语法 | 使用 * 解引用,& 取地址 |
像普通变量一样使用 |
安全性 | 较低(空指针、野指针风险) | 较高(不存在空引用,更安全) |
问: "什么时候该用指针,什么时候该用引用?"
-
使用引用:
-
函数参数传递 : 当你希望在函数内部修改外部变量,或者为了避免大型对象的值拷贝开销时,引用是首选。尤其是
const
引用,它能避免拷贝且保证数据不被修改,非常高效和安全。cppvoid process_data(const BigObject& obj); // 高效且安全
-
函数返回值 : 当函数需要返回一个容器内的元素或一个类的成员时,可以返回引用以避免拷贝。但要极其小心,绝不能返回局部变量的引用,因为函数结束后局部变量会被销毁。
-
运算符重载 : 如重载
[]
,=
等操作符时,通常返回引用,以支持链式调用(如a = b = c;
)。
-
-
使用指针:
- 表示"可能不存在" : 当一个变量可能指向一个对象,也可能什么都不指向(
nullptr
)时,必须用指针。例如,链表中的next
节点指针。 - 动态内存管理 : 在堆(Heap)上创建的对象必须通过指针来管理(例如
new
返回的就是指针)。 - 数组操作: C/C++中数组名本质上就是指向首元素的指针,指针算术对于遍历和操作数组非常方便。
- 表示"可能不存在" : 当一个变量可能指向一个对象,也可能什么都不指向(
2.C与C++的struct
对比
特性 | C 中的 struct |
C++ 中的 struct |
---|---|---|
成员类型 | 只能包含数据成员 | 可以包含数据成员和成员函数(方法) |
访问权限 | 无访问权限控制(所有成员都是公开的) | 有访问权限控制 (public , private , protected ),默认是 public |
构造/析构函数 | 没有构造函数或析构函数 | 可以有构造函数和析构函数 |
继承 | 不支持继承 | 可以 从其他 struct 或 class 继承 |
this 指针 |
没有 this 指针的概念 |
成员函数内部有 this 指针,指向当前对象 |
作为类型名 | 声明变量时必须带 struct 关键字 (如 struct Point p; ),除非使用typedef |
struct 名直接就是类型名 (如 Point p; ) |
C 语言的实现 (struct
作为数据容器)
在C语言中,struct
用来定义数据结构,而操作这些数据的函数是全局的、与之分离的。
c
#include <stdio.h>
// 1. 定义一个数据结构
struct Point {
int x;
int y;
};
// 2. 操作该数据结构的函数是全局的
void init_point(struct Point* p, int x, int y) {
p->x = x;
p->y = y;
}
void print_point(struct Point p) {
printf("Point(%d, %d)\n", p.x, p.y);
}
int main() {
// 3. 声明变量需要带 struct 关键字
struct Point p1;
// 4. 初始化和操作都是通过外部函数完成
init_point(&p1, 10, 20);
print_point(p1); // 输出: Point(10, 20)
return 0;
}
特点总结:
-
数据(
struct Point
)和行为(init_point
,print_point
)是分离的,这是典型的面向过程编程。 -
必须使用
struct Point p1;
来声明变量。为了简化,C程序员通常会用typedef
:ctypedef struct Point { int x; int y; } Point; // 现在可以用 Point p1; 来声明了
C++ 语言的实现 (struct
作为轻量级对象)
在C++中,struct
可以将数据和操作数据的行为封装在一起。
cpp
#include <iostream>
// 1. 定义一个轻量级对象
struct Point {
// 数据成员
int x;
int y;
// 2. 成员函数 (行为)
// 构造函数
Point(int x_val, int y_val) {
x = x_val;
y = y_val;
}
void print() {
// 成员函数可以直接访问成员变量
std::cout << "Point(" << x << ", " << y << ")" << std::endl;
}
};
int main() {
// 3. 声明变量时不需要 struct 关键字
Point p1(10, 20); // 4. 自动调用构造函数进行初始化
// 5. 直接调用对象的成员函数
p1.print(); // 输出: Point(10, 20)
// 成员默认是 public,可以直接访问
p1.x = 30;
p1.print(); // 输出: Point(30, 20)
return 0;
}
特点总结:
- 数据和行为被封装在了一起,这是面向对象编程的基本思想。
- 拥有构造函数,使得对象的初始化更安全、更便捷。
- 可以直接使用
Point p1;
进行声明。
3. struct
和 class
在C++中,struct
和 class
的功能几乎完全相同,唯一的核心区别 在于默认的访问权限和继承权限。
特性 | struct |
class |
---|---|---|
默认成员访问权限 | public |
private |
默认继承方式 | public |
private |
详细解释
-
默认成员访问权限:
- 在一个
struct
中,如果你不显式指定public:
,private:
, 或protected:
,那么其所有成员(包括数据和方法)默认都是public
的。 - 在一个
class
中,默认是private
的。
cppstruct MyStruct { int x; // 默认是 public void print() {} // 默认是 public }; class MyClass { int x; // 默认是 private void print() {} // 默认是 private }; int main() { MyStruct s; s.x = 10; // OK MyClass c; // c.x = 10; // 错误! 'x' is private }
- 在一个
-
默认继承方式:
- 当一个
struct
继承自另一个struct
或class
时,默认的继承方式是public
继承。 - 当一个
class
继承时,默认是private
继承。
cppclass Base { public: int y; }; struct DerivedStruct : Base { /* y is public here */ }; class DerivedClass : Base { /* y is private here */ };
- 当一个
使用约定
-
使用
struct
:- 当你只是想将一堆数据聚合在一起,作为一个数据容器时。
- 这些数据成员之间没有复杂的逻辑关系或不变量(invariant)需要维护。
- 成员默认是公开的,外部可以直接访问和修改。
- 通常称之为 POD (Plain Old Data) 类型。
- 例子 :
Point { double x, y; }
,Color { int r, g, b; }
。
-
使用
class
:- 当你需要封装数据,隐藏内部实现细节时。
- 需要通过
public
接口(成员函数)来控制对private
成员的访问。 - 需要维护类的不变量 (例如,一个
Date
类的day
成员必须在1到31之间)。 - 当你打算使用继承和多态等面向对象的特性时。
- 例子 :
DatabaseConnection
,HttpRequest
,Car
等具有复杂行为和状态的对象。
总结 : 用 struct
来表示数据 ,用 class
来表示拥有复杂行为和封装状态的对象。这是一种代码风格,能让其他开发者更快地理解你的意图。
3. new
/delete
和 malloc
/free
这两组函数都用于在堆(Heap)上动态分配和释放内存,但它们属于不同的"时代"和技术范畴。
malloc
/free
: C语言标准库函数,位于<cstdlib>
(或<stdlib.h>
)。new
/delete
: C++的运算符。
核心区别
特性 | malloc / free |
new / delete |
---|---|---|
来源 | C标准库函数 | C++运算符 |
类型安全 | 不安全 。malloc 返回 void* ,需要手动强制类型转换。 |
类型安全 。new 返回对应类型的指针,无需转换。 |
构造/析构 | 不会调用对象的构造函数和析构函数。 | new 在分配内存后会调用构造函数 ;delete 在释放内存前会调用析构函数。 |
内存大小 | 需要手动计算所需字节数,如 malloc(10 * sizeof(int)) 。 |
编译器自动计算所需大小,如 new int[10] 。 |
数组处理 | free 对单个对象和数组使用方式相同。 |
单个对象用 delete ,数组用 delete[] 。混用是严重错误。 |
重载 | 不可重载。 | 可以被类重载,实现自定义的内存分配策略。 |
异常处理 | 分配失败时返回 NULL 。 |
分配失败时默认抛出 std::bad_alloc 异常。 |
用法示例与陷阱
cpp
#include <iostream>
#include <cstdlib>
class MyObject {
public:
MyObject() { std::cout << "Constructor called" << std::endl; }
~MyObject() { std::cout << "Destructor called" << std::endl; }
};
int main() {
// --- C 风格 ---
std::cout << "Using malloc/free:" << std::endl;
MyObject* obj1 = (MyObject*)malloc(sizeof(MyObject)); // 1. 手动计算大小,强制转换
// 2. 注意:构造函数没有被调用!
free(obj1); // 3. 注意:析构函数没有被调用!
std::cout << "\n--- C++ 风格 ---" << std::endl;
MyObject* obj2 = new MyObject(); // 1. 类型安全,自动计算大小
// 2. 构造函数被自动调用
delete obj2; // 3. 析构函数被自动调用
std::cout << "\n--- C++ 数组 ---" << std::endl;
MyObject* arr = new MyObject[3]; // 调用3次构造函数
delete[] arr; // 调用3次析构函数。如果写成 delete arr; (错误!),只会调用一次析构,导致内存泄漏!
}