当前位置: 首页 > news >正文

C++学习:六个月从基础到就业——C++基础语法回顾:指针与引用基础

C++学习:六个月从基础到就业——C++基础语法回顾:指针与引用基础

本文是我C++学习之旅系列的第六篇技术文章,主要回顾C++中的指针与引用基础,包括内存模型、指针操作、引用特性以及它们的区别与应用场景。查看完整系列目录了解更多内容。

引言

指针和引用是C++中最强大也最具挑战性的特性之一,它们提供了对内存的直接访问和操作能力,使C++在系统编程、高性能计算等领域具有显著优势。同时,不正确地使用指针也是导致内存泄漏、段错误和其他难以调试的问题的主要原因。本文将详细介绍C++中指针与引用的基础概念、语法和使用方法,帮助你构建对这些重要特性的清晰理解。

内存模型基础

在深入指针和引用之前,我们需要了解C++程序的内存模型:

内存布局

C++程序的内存通常分为以下几个部分:

  1. 代码段(Text Segment):存储程序的机器代码。
  2. 数据段(Data Segment):存储全局变量和静态变量。
  3. 堆(Heap):用于动态内存分配,程序员负责管理。
  4. 栈(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;  // 错误:引用必须初始化

引用与指针的区别

  1. 引用必须初始化,指针可以不初始化
  2. 引用初始化后不能改变指向,指针可以随时改变
  3. 没有"空引用",但有空指针
  4. 引用总是指向某个对象,访问安全性高于指针
  5. 语法上,引用使用更简单(不需要解引用操作符)
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;
}

指针与引用的应用场景

何时使用指针

  1. 需要表示"没有对象"的概念(空指针)
  2. 需要能够改变引用的对象
  3. 进行动态内存分配
  4. 处理复杂的数据结构(如链表、树)
  5. 需要指针算术运算

何时使用引用

  1. 函数参数需要修改调用者提供的对象
  2. 函数参数是大型对象,想避免复制但不会修改它(const引用)
  3. 在运算符重载中
  4. 需要确保变量始终引用有效对象

指针和引用的选择建议

  1. 优先使用引用,因为它更安全、语法更简单
  2. 如果需要"无对象"的概念或需要改变指向,使用指针
  3. 对于类成员变量,通常使用指针表示可选关系,使用引用表示必需关系
  4. 现代C++中,优先考虑使用智能指针而非原始指针

常见陷阱与错误

未初始化指针

int* p;  // 未初始化,包含垃圾值
*p = 10;  // 危险:可能导致段错误

解决方法:总是初始化指针,通常为nullptr。

内存泄漏

void func() {int* p = new int[1000];// 忘记 delete[] p;
}  // 退出函数时内存泄漏

解决方法:使用智能指针或确保每个new都有对应的delete。

垂悬指针/引用

int* createNumber() {int num = 42;return &num;  // 错误:返回局部变量的地址
}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;  // 错误:空指针解引用,导致段错误

解决方法:在解引用前检查指针是否为空。

最佳实践

  1. 始终初始化指针

    int* p = nullptr;  // 明确表示指针不指向任何对象
    
  2. 使用智能指针

    std::unique_ptr<int> p = std::make_unique<int>(42);
    std::shared_ptr<int> sp = std::make_shared<int>(42);
    
  3. 引用参数使用const

    void func(const std::string& s);  // 避免复制,且不修改s
    
  4. 指针参数验证

    void process(int* p) {if (!p) return;  // 检查空指针// 处理p指向的数据
    }
    
  5. 使用nullptr而非NULL或0

    int* p = nullptr;  // C++11推荐方式
    
  6. 避免裸指针管理资源

    // 不好的方式
    File* f = new File("data.txt");
    // 使用f...
    delete f;// 好的方式
    std::unique_ptr<File> f = std::make_unique<File>("data.txt");
    // 使用f...
    // 自动释放资源
    
  7. 避免复杂的指针类型

    // 难以理解
    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++中最强大且最具挑战性的特性。指针提供了对内存的直接访问和操作能力,但也带来了内存泄漏、段错误等风险。引用则提供了更安全、更简洁的间接访问方式,但缺乏部分指针的灵活性。

关键点回顾:

  1. 指针存储内存地址,通过解引用操作符(*)访问数据
  2. 引用是对象的别名,必须初始化且不能改变所引用的对象
  3. 空指针(nullptr)表示指针不指向任何对象
  4. 指针算术允许在内存中移动指针
  5. new/delete用于动态内存分配和释放
  6. 智能指针(unique_ptr、shared_ptr、weak_ptr)提供自动内存管理
  7. const与指针结合形成多种组合,控制修改权限
  8. 函数指针允许存储和调用函数,用于回调和策略模式
  9. 右值引用(&&)支持移动语义,提高性能

在实际编程中,应该优先使用更安全的抽象(如智能指针、容器类)而非原始指针,遵循RAII原则管理资源,避免常见陷阱如内存泄漏和垂悬指针。理解并正确使用指针和引用,是掌握C++语言的重要一步。

在下一篇文章中,我们将探讨C++中的结构体与枚举,这些是组织和表示数据的重要工具。

参考资料

  1. Bjarne Stroustrup. The C++ Programming Language (4th Edition)
  2. Scott Meyers. Effective Modern C++
  3. cppreference.com - 指针
  4. cppreference.com - 引用
  5. C++ Core Guidelines - 指针和引用

这是我C++学习之旅系列的第六篇技术文章。查看完整系列目录了解更多内容。


http://www.mrgr.cn/news/95879.html

相关文章:

  • html和css 实现元素顺时针旋转效果(椭圆形旋转轨迹)
  • 【react】在react中async/await一般用来实现什么功能
  • 【Java】Springboot集成itextpdf制作pdf(内附pdf添加表格、背景图、水印,条形码、二维码,页码等功能)
  • 从医疗大模型到综合医疗智能体:算法、架构与路径全流程分析
  • aws S3利用lambda edge实现图片缩放、质量转换等常规图片处理功能
  • Java 线程池全面解析
  • Linux输入系统应用编程
  • 【linux重设gitee账号密码 克隆私有仓库报错】
  • 3、孪生网络/连体网络(Siamese Network)
  • 【WebGIS教程1】WebGIS学习初步知识了解 · 概述
  • 2025最新版Ubuntu Server版本Ubuntu 24.04.2 LTS下载与安装-详细教程,细致到每一步都有说明
  • Linux--环境变量
  • 向量数据库学习笔记(1) —— 基础概念
  • djinn: 1靶场渗透测试
  • 微服务面试题:分布式事务和服务监控
  • 中学数学几百年重大错误:将无穷多各异假R误为R——两数集相等的必要条件
  • 万字C++STL——vector模拟实现
  • STM32内部时钟输出比较OC(学习笔记)
  • 常用的离散时间傅里叶变换(DTFT)对
  • Langchain中的表格解析:RAG 和表格的爱恨情仇