C++学习:六个月从基础到就业——C++基础语法回顾:指针与引用基础
C++学习:六个月从基础到就业——C++基础语法回顾:指针与引用基础
本文是我C++学习之旅系列的第六篇技术文章,主要回顾C++中的指针与引用基础,包括内存模型、指针操作、引用特性以及它们的区别与应用场景。查看完整系列目录了解更多内容。
引言
指针和引用是C++中最强大也最具挑战性的特性之一,它们提供了对内存的直接访问和操作能力,使C++在系统编程、高性能计算等领域具有显著优势。同时,不正确地使用指针也是导致内存泄漏、段错误和其他难以调试的问题的主要原因。本文将详细介绍C++中指针与引用的基础概念、语法和使用方法,帮助你构建对这些重要特性的清晰理解。
内存模型基础
在深入指针和引用之前,我们需要了解C++程序的内存模型:
内存布局
C++程序的内存通常分为以下几个部分:
- 代码段(Text Segment):存储程序的机器代码。
- 数据段(Data Segment):存储全局变量和静态变量。
- 堆(Heap):用于动态内存分配,程序员负责管理。
- 栈(Stack):用于存储局部变量、函数参数和返回地址。
变量与内存地址
每个变量在内存中都占据一定空间,并拥有一个唯一的内存地址:
int number = 42;
std::cout << "number的值: " << number << std::endl;
std::cout << "number的地址: " << &number << std::endl;
上面代码中,&number
返回变量number
的内存地址,这是指针概念的基础。
指针基础
什么是指针
指针是一种存储内存地址的变量。通过指针,我们可以间接访问存储在该地址的数据。
指针声明与初始化
声明一个指针变量的语法如下:
type* pointer_name; // 或 type *pointer_name;
初始化指针:
int number = 42;
int* p = &number; // p指向number的地址
空指针初始化:
int* p = nullptr; // C++11推荐的空指针初始化方式
int* p = NULL; // 传统C风格,等同于int* p = 0;
int* p = 0; // 直接使用0,不推荐
解引用操作符
解引用操作符(*
)用于访问指针指向的内存位置的值:
int number = 42;
int* p = &number;
std::cout << *p << std::endl; // 输出42,即p指向的值*p = 100; // 通过指针修改值
std::cout << number << std::endl; // 输出100
指针的类型
指针变量的类型决定了解引用时如何解释内存中的数据:
int number = 0x12345678; // 十六进制表示
int* p_int = &number;
char* p_char = (char*)&number; // 需要强制类型转换std::cout << *p_int << std::endl; // 输出整数305419896(0x12345678)
std::cout << (int)*p_char << std::endl; // 输出78或120(取决于系统的字节序)
指针算术
指针支持简单的算术操作:
int arr[5] = {10, 20, 30, 40, 50};
int* p = arr; // p指向数组首元素std::cout << *p << std::endl; // 输出10
std::cout << *(p + 1) << std::endl; // 输出20
std::cout << *(p + 2) << std::endl; // 输出30
指针递增或递减会根据指针类型调整偏移量:
char* p_char = new char[5];
int* p_int = new int[5];std::cout << "p_char: " << (void*)p_char << std::endl;
std::cout << "p_char + 1: " << (void*)(p_char + 1) << std::endl; // 地址加1字节std::cout << "p_int: " << p_int << std::endl;
std::cout << "p_int + 1: " << (p_int + 1) << std::endl; // 地址加4字节(在大多数系统上)delete[] p_char;
delete[] p_int;
const与指针
const可以和指针结合使用,形成几种不同的组合:
// 指向常量的指针(不能通过指针修改所指对象)
const int* p1 = &number;
// *p1 = 50; // 错误:不能通过p1修改number// 常量指针(指针本身不能改变)
int* const p2 = &number;
*p2 = 50; // 正确:可以修改number
// p2 = &another; // 错误:p2不能指向其他变量// 指向常量的常量指针(两者都不能改变)
const int* const p3 = &number;
// *p3 = 50; // 错误
// p3 = &another; // 错误
记忆技巧:读从右到左。例如,const int* p
读作"p是一个指向const int的指针"。
void指针
void指针可以存储任何类型对象的地址,但在使用前需要转换为具体类型:
int number = 42;
void* p = &number; // 存储int的地址// 使用前需要转换
int* p_int = static_cast<int*>(p);
std::cout << *p_int << std::endl; // 输出42// C风格转换(不推荐)
int* p_int2 = (int*)p;
多重指针
指针也可以指向另一个指针,形成多级间接引用:
int number = 42;
int* p = &number; // 指向int的指针
int** pp = &p; // 指向指针的指针std::cout << **pp << std::endl; // 输出42
**pp = 100; // 修改number的值
std::cout << number << std::endl; // 输出100
指针与数组
数组名与指针
数组名在大多数情况下会退化为指向首元素的指针:
int arr[5] = {10, 20, 30, 40, 50};
int* p = arr; // arr退化为&arr[0]std::cout << *p << std::endl; // 输出10
std::cout << *(arr + 1) << std::endl; // 输出20,等同于arr[1]
主要区别:
- 数组名是常量,不能修改指向(不能执行
arr++
) - sizeof(arr)返回整个数组的大小,而sizeof§返回指针的大小
数组的指针算术
通过指针可以遍历数组:
int arr[5] = {10, 20, 30, 40, 50};
int* p = arr;for (int i = 0; i < 5; i++) {std::cout << *(p + i) << " "; // 等同于p[i]或arr[i]
}
std::cout << std::endl;// 或者直接递增指针
p = arr; // 重置指针位置
for (int i = 0; i < 5; i++) {std::cout << *p << " ";p++;
}
std::cout << std::endl;
指针数组
指针数组是存储指针的数组:
int a = 10, b = 20, c = 30;
int* arr[3]; // 存储int指针的数组arr[0] = &a;
arr[1] = &b;
arr[2] = &c;for (int i = 0; i < 3; i++) {std::cout << *arr[i] << " "; // 输出10 20 30
}
std::cout << std::endl;
数组指针
数组指针是指向数组的指针:
int arr[3] = {10, 20, 30};
int (*p)[3] = &arr; // p是指向包含3个int的数组的指针std::cout << (*p)[0] << " " << (*p)[1] << " " << (*p)[2] << std::endl; // 输出10 20 30
指针与字符串
字符串字面量与指针
字符串字面量可以用字符指针表示:
const char* str = "Hello"; // str指向字符串常量// 注意:不能修改字符串字面量
// str[0] = 'h'; // 错误:未定义行为// 如果需要修改,应该使用数组
char str_array[] = "Hello";
str_array[0] = 'h'; // 合法
字符指针数组
常用于程序参数、命令行参数等:
const char* fruits[] = {"apple", "banana", "cherry"};for (int i = 0; i < 3; i++) {std::cout << fruits[i] << std::endl;
}
动态内存管理
new和delete操作符
C++使用new和delete操作符进行动态内存分配:
// 分配单个对象
int* p = new int;
*p = 42;
std::cout << *p << std::endl;
delete p; // 释放内存// 分配并初始化
int* q = new int(100);
std::cout << *q << std::endl;
delete q;// 分配数组
int* arr = new int[5]{10, 20, 30, 40, 50};
for (int i = 0; i < 5; i++) {std::cout << arr[i] << " ";
}
std::cout << std::endl;
delete[] arr; // 注意使用delete[]释放数组
内存泄漏
未正确释放动态分配的内存会导致内存泄漏:
void memory_leak() {int* p = new int(42);// 忘记调用delete p; -- 内存泄漏
}
垂悬指针
指向已释放内存的指针称为垂悬指针(dangling pointer):
int* p = new int(42);
delete p; // p现在是垂悬指针
// *p = 100; // 未定义行为,可能导致崩溃// 好习惯:释放后将指针设为nullptr
p = nullptr;
if (p) { // 检查指针是否有效*p = 100;
}
智能指针简介
现代C++提供了智能指针来自动管理内存,避免内存泄漏:
#include <memory>// unique_ptr:独占所有权
std::unique_ptr<int> up = std::make_unique<int>(42); // C++14
std::cout << *up << std::endl;
// 离开作用域时自动释放// shared_ptr:共享所有权
std::shared_ptr<int> sp1 = std::make_shared<int>(100);
std::shared_ptr<int> sp2 = sp1; // 两个指针共享同一对象
std::cout << *sp1 << " " << *sp2 << std::endl;
// 当最后一个shared_ptr销毁时释放对象
引用基础
什么是引用
引用是一个变量的别名,必须初始化,并且初始化后不能改变引用的对象。
引用声明与初始化
int number = 42;
int& ref = number; // ref是number的引用std::cout << ref << std::endl; // 输出42
ref = 100; // 修改number的值
std::cout << number << std::endl; // 输出100
引用必须在声明时初始化:
int& ref; // 错误:引用必须初始化
引用与指针的区别
- 引用必须初始化,指针可以不初始化
- 引用初始化后不能改变指向,指针可以随时改变
- 没有"空引用",但有空指针
- 引用总是指向某个对象,访问安全性高于指针
- 语法上,引用使用更简单(不需要解引用操作符)
int a = 10, b = 20;
int& ref = a; // ref引用a
int* ptr = &a; // ptr指向a// 修改引用所指的变量
ref = 100; // a变为100
std::cout << a << std::endl;// 修改指针所指的变量
*ptr = 200; // a变为200
std::cout << a << std::endl;// 改变指针指向
ptr = &b; // 现在ptr指向b
*ptr = 300; // b变为300
std::cout << a << " " << b << std::endl;// 引用不能改变指向
// ref = b; // 这并非改变引用指向,而是将b的值赋给a
const引用
const引用可以引用常量或临时对象:
const int& ref1 = 42; // 引用字面量(临时对象)
int x = 10;
const int& ref2 = x; // 引用变量,但不能通过ref2修改x
// ref2 = 20; // 错误:不能通过const引用修改值
const引用常用于函数参数,避免复制大对象:
void printString(const std::string& s) {std::cout << s << std::endl;// s = "modified"; // 错误:不能修改
}
引用作为函数参数
引用作为函数参数可以避免复制,同时允许函数修改实参:
// 通过引用修改参数
void increment(int& num) {num++;
}// 使用const引用避免复制大对象
void printVector(const std::vector<int>& vec) {for (int val : vec) {std::cout << val << " ";}std::cout << std::endl;
}int main() {int x = 10;increment(x);std::cout << x << std::endl; // 输出11std::vector<int> numbers = {1, 2, 3, 4, 5};printVector(numbers);return 0;
}
引用作为函数返回值
函数可以返回引用,但需要注意不要返回局部变量的引用:
// 安全:返回全局或静态变量的引用
int& getGlobalRef() {static int global_var = 42;return global_var;
}// 危险:返回局部变量的引用
int& getDangling() {int local_var = 10;return local_var; // 危险:函数返回后local_var不再存在
}int main() {int& ref = getGlobalRef();ref = 100;std::cout << getGlobalRef() << std::endl; // 输出100// int& dangerous = getDangling(); // 垂悬引用,未定义行为return 0;
}
右值引用(C++11)
C++11引入了右值引用,主要用于移动语义和完美转发:
// 右值引用使用&&声明
int&& rref = 42; // 绑定到字面量(右值)// 左值引用不能绑定到右值
// int& lref = 42; // 错误,除非使用const引用// 但右值引用不能绑定到左值
int x = 10;
// int&& rref2 = x; // 错误// 除非使用std::move将左值转换为右值引用
int&& rref3 = std::move(x); // 正确
右值引用的主要用途是实现移动语义,避免不必要的复制:
#include <vector>
#include <string>
#include <utility> // 为std::movevoid moveExample() {std::string str = "Hello";std::vector<std::string> vec;// 移动而非复制,将str的内容转移到vector中vec.push_back(std::move(str));// 此时str可能为空(取决于具体实现)std::cout << "str after move: " << str << std::endl;std::cout << "vec[0]: " << vec[0] << std::endl;
}
函数指针
函数指针是指向函数的指针,可用于实现回调和策略模式:
// 定义函数
int add(int a, int b) {return a + b;
}int subtract(int a, int b) {return a - b;
}int main() {// 声明函数指针int (*operation)(int, int);// 指向add函数operation = add;std::cout << operation(5, 3) << std::endl; // 输出8// 指向subtract函数operation = subtract;std::cout << operation(5, 3) << std::endl; // 输出2return 0;
}
函数指针可以作为函数参数,实现回调:
// 使用函数指针作为参数
void processNumbers(int* arr, int size, int (*processor)(int)) {for (int i = 0; i < size; i++) {arr[i] = processor(arr[i]);}
}// 回调函数
int square(int x) {return x * x;
}int doubleValue(int x) {return x * 2;
}int main() {int numbers[5] = {1, 2, 3, 4, 5};// 将所有元素平方processNumbers(numbers, 5, square);for (int i = 0; i < 5; i++) {std::cout << numbers[i] << " "; // 输出1 4 9 16 25}std::cout << std::endl;// 将所有元素乘以2processNumbers(numbers, 5, doubleValue);for (int i = 0; i < 5; i++) {std::cout << numbers[i] << " "; // 输出2 8 18 32 50}std::cout << std::endl;return 0;
}
指针与引用的应用场景
何时使用指针
- 需要表示"没有对象"的概念(空指针)
- 需要能够改变引用的对象
- 进行动态内存分配
- 处理复杂的数据结构(如链表、树)
- 需要指针算术运算
何时使用引用
- 函数参数需要修改调用者提供的对象
- 函数参数是大型对象,想避免复制但不会修改它(const引用)
- 在运算符重载中
- 需要确保变量始终引用有效对象
指针和引用的选择建议
- 优先使用引用,因为它更安全、语法更简单
- 如果需要"无对象"的概念或需要改变指向,使用指针
- 对于类成员变量,通常使用指针表示可选关系,使用引用表示必需关系
- 现代C++中,优先考虑使用智能指针而非原始指针
常见陷阱与错误
未初始化指针
int* p; // 未初始化,包含垃圾值
*p = 10; // 危险:可能导致段错误
解决方法:总是初始化指针,通常为nullptr。
内存泄漏
void func() {int* p = new int[1000];// 忘记 delete[] p;
} // 退出函数时内存泄漏
解决方法:使用智能指针或确保每个new都有对应的delete。
垂悬指针/引用
int* createNumber() {int num = 42;return # // 错误:返回局部变量的地址
}int& createRef() {int num = 42;return num; // 错误:返回局部变量的引用
}
解决方法:不要返回局部变量的指针或引用。
越界访问
int arr[5] = {1, 2, 3, 4, 5};
int* p = arr;
std::cout << p[10] << std::endl; // 错误:越界访问
解决方法:确保指针访问在有效范围内,现代C++中优先使用std::vector和std::array。
对空指针解引用
int* p = nullptr;
*p = 10; // 错误:空指针解引用,导致段错误
解决方法:在解引用前检查指针是否为空。
最佳实践
-
始终初始化指针:
int* p = nullptr; // 明确表示指针不指向任何对象
-
使用智能指针:
std::unique_ptr<int> p = std::make_unique<int>(42); std::shared_ptr<int> sp = std::make_shared<int>(42);
-
引用参数使用const:
void func(const std::string& s); // 避免复制,且不修改s
-
指针参数验证:
void process(int* p) {if (!p) return; // 检查空指针// 处理p指向的数据 }
-
使用nullptr而非NULL或0:
int* p = nullptr; // C++11推荐方式
-
避免裸指针管理资源:
// 不好的方式 File* f = new File("data.txt"); // 使用f... delete f;// 好的方式 std::unique_ptr<File> f = std::make_unique<File>("data.txt"); // 使用f... // 自动释放资源
-
避免复杂的指针类型:
// 难以理解 int* (*fp)(int**, char* const[]);// 使用using或typedef简化 using FuncPtr = int* (*)(int**, char* const[]); FuncPtr fp;
实际应用案例
案例1:简单链表实现
#include <iostream>// 链表节点结构
struct Node {int data;Node* next;Node(int val) : data(val), next(nullptr) {}
};// 简单链表类
class LinkedList {
private:Node* head;public:LinkedList() : head(nullptr) {}// 析构函数释放所有节点内存~LinkedList() {Node* current = head;while (current) {Node* next = current->next;delete current;current = next;}}// 在链表头添加节点void prepend(int val) {Node* newNode = new Node(val);newNode->next = head;head = newNode;}// 在链表尾添加节点void append(int val) {Node* newNode = new Node(val);if (!head) {head = newNode;return;}Node* current = head;while (current->next) {current = current->next;}current->next = newNode;}// 删除指定值的节点void remove(int val) {if (!head) return;// 如果头节点匹配if (head->data == val) {Node* temp = head;head = head->next;delete temp;return;}// 检查其他节点Node* current = head;while (current->next && current->next->data != val) {current = current->next;}if (current->next) {Node* temp = current->next;current->next = temp->next;delete temp;}}// 打印链表void print() const {Node* current = head;while (current) {std::cout << current->data << " -> ";current = current->next;}std::cout << "nullptr" << std::endl;}
};int main() {LinkedList list;list.append(1);list.append(2);list.append(3);list.prepend(0);std::cout << "原始链表: ";list.print();list.remove(2);std::cout << "删除2后: ";list.print();return 0;
}
案例2:函数回调系统
#include <iostream>
#include <vector>
#include <functional> // 用于std::function// 使用std::function实现更灵活的回调系统
class CallbackSystem {
private:std::vector<std::function<void(int)>> callbacks;public:// 注册回调函数void registerCallback(const std::function<void(int)>& callback) {callbacks.push_back(callback);}// 触发所有回调void triggerCallbacks(int value) {for (const auto& callback : callbacks) {callback(value);}}
};// 普通函数回调
void printValue(int value) {std::cout << "回调值: " << value << std::endl;
}// 带状态的函数对象
class ValueModifier {
private:int modifier;public:ValueModifier(int mod) : modifier(mod) {}void operator()(int value) const {std::cout << "修改后的值: " << value * modifier << std::endl;}
};int main() {CallbackSystem system;// 注册普通函数system.registerCallback(printValue);// 注册函数对象ValueModifier doubler(2);system.registerCallback(doubler);// 注册lambda表达式system.registerCallback([](int value) {std::cout << "Lambda: " << value << "的平方是" << value * value << std::endl;});// 触发回调system.triggerCallbacks(5);return 0;
}
案例3:智能指针管理资源
#include <iostream>
#include <memory>
#include <vector>
#include <string>// 模拟资源类
class Resource {
private:std::string name;public:Resource(const std::string& n) : name(n) {std::cout << "Resource " << name << " created." << std::endl;}~Resource() {std::cout << "Resource " << name << " destroyed." << std::endl;}void use() const {std::cout << "Using resource " << name << std::endl;}
};// 展示unique_ptr用法
void uniquePtrDemo() {std::cout << "\n=== unique_ptr演示 ===" << std::endl;// 创建unique_ptrstd::unique_ptr<Resource> res1 = std::make_unique<Resource>("First");// 使用资源res1->use();// 无法复制unique_ptr// std::unique_ptr<Resource> res2 = res1; // 编译错误// 但可以移动所有权std::unique_ptr<Resource> res2 = std::move(res1);// 原指针现在为空if (!res1) {std::cout << "res1现在为空" << std::endl;}// res2拥有资源res2->use();// 在函数结束时,res2自动销毁,调用Resource析构函数
}// 展示shared_ptr用法
void sharedPtrDemo() {std::cout << "\n=== shared_ptr演示 ===" << std::endl;// 创建shared_ptrstd::shared_ptr<Resource> res1 = std::make_shared<Resource>("Shared");{// 创建共享资源的第二个指针std::shared_ptr<Resource> res2 = res1;std::cout << "引用计数: " << res1.use_count() << std::endl; // 输出2// 两个指针都可以使用资源res1->use();res2->use();// 作用域结束,res2销毁,但资源依然存在}std::cout << "引用计数: " << res1.use_count() << std::endl; // 输出1res1->use();// 在函数结束时,res1销毁,引用计数变为0,资源被释放
}// 展示weak_ptr用法
void weakPtrDemo() {std::cout << "\n=== weak_ptr演示 ===" << std::endl;// weak_ptr用于解决shared_ptr循环引用问题std::shared_ptr<Resource> res = std::make_shared<Resource>("Weak Demo");// 创建weak_ptr,不增加引用计数std::weak_ptr<Resource> weak = res;std::cout << "引用计数: " << res.use_count() << std::endl; // 输出1// 使用weak_ptr前需要检查资源是否存在if (auto shared = weak.lock()) {shared->use();} else {std::cout << "资源已不存在" << std::endl;}// 释放原始shared_ptrres.reset();// 现在weak_ptr已经失效if (auto shared = weak.lock()) {shared->use();} else {std::cout << "资源已不存在" << std::endl;}
}int main() {uniquePtrDemo();sharedPtrDemo();weakPtrDemo();return 0;
}
总结
指针和引用是C++中最强大且最具挑战性的特性。指针提供了对内存的直接访问和操作能力,但也带来了内存泄漏、段错误等风险。引用则提供了更安全、更简洁的间接访问方式,但缺乏部分指针的灵活性。
关键点回顾:
- 指针存储内存地址,通过解引用操作符(
*
)访问数据 - 引用是对象的别名,必须初始化且不能改变所引用的对象
- 空指针(nullptr)表示指针不指向任何对象
- 指针算术允许在内存中移动指针
- new/delete用于动态内存分配和释放
- 智能指针(unique_ptr、shared_ptr、weak_ptr)提供自动内存管理
- const与指针结合形成多种组合,控制修改权限
- 函数指针允许存储和调用函数,用于回调和策略模式
- 右值引用(&&)支持移动语义,提高性能
在实际编程中,应该优先使用更安全的抽象(如智能指针、容器类)而非原始指针,遵循RAII原则管理资源,避免常见陷阱如内存泄漏和垂悬指针。理解并正确使用指针和引用,是掌握C++语言的重要一步。
在下一篇文章中,我们将探讨C++中的结构体与枚举,这些是组织和表示数据的重要工具。
参考资料
- Bjarne Stroustrup. The C++ Programming Language (4th Edition)
- Scott Meyers. Effective Modern C++
- cppreference.com - 指针
- cppreference.com - 引用
- C++ Core Guidelines - 指针和引用
这是我C++学习之旅系列的第六篇技术文章。查看完整系列目录了解更多内容。