C++笔记之指针

C++ 中的指针是其最核心和强大的特性之一,但也是初学者容易混淆的地方。它允许你直接操作内存地址,实现高效的算法和数据结构。

1. 指针的核心概念

指针 (Pointer) 本质上是一个变量,但它存储的不是普通的值(如整数、字符),而是另一个变量的内存地址

核心操作符:

  • & (取地址符 / Address-of Operator): 获取一个变量的内存地址。

  • * (解引用符 / Dereference Operator): 访问指针所指向地址上的值。

2. 指针的声明、初始化与使用

2.1 声明指针

声明一个指针时,需要指定它将要指向的数据类型。

语法:

type *pointer_name;

int *p_int;       // 声明一个指向 int 类型的指针
double *p_double; // 声明一个指向 double 类型的指针
std::string *p_str; // 声明一个指向 string 对象的指针

注意: * 的位置可以紧跟类型 int* p;,也可以紧跟变量名 int *p;,或者在中间 int * p;。它们的含义完全相同,但 int* p1, p2; 这种写法容易误导,它表示 p1 是一个 int 指针,而 p2 只是一个普通的 int 变量。因此,推荐将 * 与变量名写在一起。

2.2 初始化指针

指针在使用前必须被初始化,即让它指向一个有效的内存地址或 nullptr。未初始化的指针(野指针)非常危险。

#include <iostream>
#include <string>

int main() {
    int var = 42;
    std::string greeting = "Hello";

    // 1. 声明的同时初始化
    int *p_var = &var; // p_var 存储了 var 的地址

    // 2. 先声明后初始化
    std::string *p_greeting;
    p_greeting = &greeting;

    // 访问地址
    std::cout << "Address of var: " << p_var << std::endl;
    std::cout << "Address of greeting: " << p_greeting << std::endl;
}
2.3 解引用:访问值

使用解引用符 * 可以获取或修改指针指向地址上的数据。

#include <iostream>

int main() {
    int score = 90;
    int *p_score = &score;

    // 读取值
    std::cout << "Value of score (via variable): " << score << std::endl;
    std::cout << "Value of score (via pointer): " << *p_score << std::endl;

    // 修改值
    *p_score = 95; // 通过指针修改了 score 的值
    std::cout << "New value of score: " << score << std::endl; // 输出 95
}
2.4 空指针 (Null Pointer)

空指针是一个不指向任何有效内存地址的特殊指针。在 C++11 及之后,强烈推荐使用 nullptr 来表示空指针。

int *p_safe = nullptr; // 推荐的现代 C++ 写法
// int *p_legacy = NULL; // C 风格的写法,在 C++ 中应避免

if (p_safe == nullptr) {
    std::cout << "This is a null pointer." << std::endl;
}

3. 指针与数组

指针和数组在 C++ 中关系紧密。数组名本身就可以被看作一个指向数组第一个元素的常量指针。

3.1 数组名即指针
#include <iostream>

int main() {
    int numbers[] = {10, 20, 30, 40, 50};

    int *p_numbers = numbers; // 数组名直接赋值给指针

    std::cout << "First element: " << *p_numbers << std::endl; // 输出 10
    std::cout << "Address of first element: " << p_numbers << std::endl;
    std::cout << "Address of the array:     " << &numbers[0] << std::endl;
}
3.2 指针算术

可以对指针进行加减运算,使其在内存中前后移动。指针的每一步移动,跨越的字节数等于其所指向数据类型的大小。

#include <iostream>

int main() {
    int numbers[] = {10, 20, 30, 40, 50};
    int *ptr = numbers; // ptr 指向 numbers[0]

    // 移动指针
    ptr++; // ptr 现在指向 numbers[1]
    std::cout << "Second element: " << *ptr << std::endl; // 输出 20

    // 使用指针算术访问数组元素
    std::cout << "Third element: " << *(ptr + 1) << std::endl; // 输出 30
    std::cout << "First element again: " << *(ptr - 1) << std::endl; // 输出 10

    // 遍历数组
    std::cout << "Traversing array with a pointer:" << std::endl;
    for (int *p = numbers; p < numbers + 5; ++p) {
        std::cout << *p << " ";
    }
    std::cout << std::endl;
}

注意: *(ptr + i) 在功能上等价于 ptr[i]


4. const 与指针

const 关键字可以用来修饰指针,产生不同的效果。

  1. 指向常量的指针 (Pointer to const)

    • 语法: const type *ptr; 或 type const *ptr;

    • 含义: 不能通过这个指针来修改它所指向的值,但指针本身可以指向其他地址。

    int val = 10;
    const int *p_const_val = &val;
    // *p_const_val = 20; // 错误!不能通过指针修改值
    val = 20; // 正确,直接修改变量是可以的
    int another_val = 30;
    p_const_val = &another_val; // 正确,指针可以指向别处
    
  2. 常量指针 (const Pointer)

    • 语法: type * const ptr;

    • 含义: 指针本身是一个常量,它必须在声明时初始化,之后不能再指向其他地址,但可以通过它修改所指向的值。

    int val = 10;
    int * const const_p = &val;
    *const_p = 20; // 正确,可以修改指向的值
    int another_val = 30;
    // const_p = &another_val; // 错误!指针不能指向别处
    
  3. 指向常量的常量指针 (const Pointer to const)

    • 语法: const type * const ptr;

    • 含义: 指针本身和它所指向的值都不能被修改。

    int val = 10;
    const int * const const_p_const_val = &val;
    // *const_p_const_val = 20; // 错误!
    // const_p_const_val = &another_val; // 错误!
    

5. 指针与函数

5.1 指针作为函数参数(传址调用)

将指针传递给函数,可以在函数内部修改实参的值。

#include <iostream>

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    int x = 5, y = 10;
    std::cout << "Before swap: x = " << x << ", y = " << y << std::endl;
    swap(&x, &y);
    std::cout << "After swap: x = " << x << ", y = " << y << std::endl;
}
5.2 从函数返回指针

函数可以返回一个指针。但绝对不要返回指向函数内部局部变量的指针,因为函数执行完毕后,局部变量的内存会被释放,返回的指针将成为悬挂指针 (dangling pointer)。

int* create_on_heap() {
    // 在堆上分配内存,这是安全的
    int *p = new int(100);
    return p;
}

int* create_on_stack() {
    int local_var = 50;
    // 危险!返回局部变量的地址
    return &local_var; // 函数结束后 local_var 被销毁
}

int main() {
    int *p1 = create_on_heap();
    std::cout << *p1 << std::endl; // 正确,输出 100
    delete p1; // 记得释放内存

    int *p2 = create_on_stack();
    // std::cout << *p2 << std::endl; // 未定义行为!非常危险!
}
5.3 函数指针

函数指针存储的是一个函数的地址,允许你像调用普通函数一样通过指针来调用函数。

语法:

return_type (*pointer_name)(parameter_types);

#include <iostream>

int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }

int main() {
    // 声明并初始化一个函数指针
    int (*p_func)(int, int);
    
    p_func = add; // 指向 add 函数
    int result1 = p_func(5, 3); // 通过指针调用函数
    std::cout << "Addition: " << result1 << std::endl;

    p_func = subtract; // 指向 subtract 函数
    int result2 = p_func(5, 3);
    std::cout << "Subtraction: " << result2 << std::endl;
}

6. 动态内存分配

程序运行时,可以在堆 (Heap) 上动态地请求内存。这必须通过指针来完成。

  • new:分配内存。

  • delete:释放内存。

6.1 单个对象
int *p_int = new int(25); // 分配一个 int,并初始化为 25
double *p_double = new double; // 分配一个 double,未初始化

// ... 使用指针 ...

// 必须手动释放,否则会造成内存泄漏
delete p_int;
delete p_double;

// 释放后,最好将指针设为 nullptr,防止悬挂指针
p_int = nullptr;
p_double = nullptr;
6.2 动态数组
int size = 10;
int *p_array = new int[size]; // 分配一个包含 10 个 int 的数组

for (int i = 0; i < size; ++i) {
    p_array[i] = i * 2;
}

// ... 使用数组 ...

// 释放动态数组必须使用 delete[]
delete[] p_array;
p_array = nullptr;

重要: new 和 deletenew[] 和 delete[] 必须配对使用,否则会导致未定义行为。


7. 指针与类/结构体

7.1 指向对象的指针
#include <iostream>
#include <string>

struct Student {
    std::string name;
    int id;
};

int main() {
    Student s1 = {"Alice", 101};
    Student *p_student = &s1;

    // 访问成员的两种方式
    // 1. 先解引用,再用点号 .
    std::cout << "Name: " << (*p_student).name << std::endl;
    // 2. 使用箭头操作符 -> (更常用,更简洁)
    std::cout << "ID: " << p_student->id << std::endl;
}
7.2 this 指针

在类的非静态成员函数中,this 是一个隐藏的指针,它指向调用该成员函数的对象实例。

class MyClass {
public:
    void setID(int id) {
        this->id = id; // 使用 this-> 来明确表示是成员变量
    }
    int getID() {
        return id;
    }
private:
    int id;
};

int main() {
    MyClass obj;
    obj.setID(123);
    std::cout << obj.getID() << std::endl;
}

8. 现代 C++:智能指针 (Smart Pointers)

为了解决手动管理内存(new/delete)带来的内存泄漏和悬挂指针等问题,现代 C++ 引入了智能指针。它们是行为类似于指针的类对象,利用 RAII (Resource Acquisition Is Initialization) 技术,在对象生命周期结束时自动释放所管理的内存。

需要包含头文件 <memory>

  1. std::unique_ptr (独占指针)

    • 特性: 独占所指向的对象,保证同一时间只有一个 unique_ptr 指向一个对象。它无法被复制,但可以被移动。

    • 用途: 当你需要一个指针明确地“拥有”一块内存,并确保它在指针离开作用域时被释放。

    #include <memory>
    
    std::unique_ptr<int> p1(new int(100));
    // std::unique_ptr<int> p2 = p1; // 错误!不能复制
    std::unique_ptr<int> p3 = std::move(p1); // 正确,所有权转移给 p3
    // 此时 p1 变为空指针
    
    // 当 p3 离开作用域时,它指向的内存会被自动 delete
    
  2. std::shared_ptr (共享指针)

    • 特性: 允许多个 shared_ptr 指向同一个对象。内部使用引用计数,当最后一个指向对象的 shared_ptr被销毁时,内存才会被释放。

    • 用途: 当多个地方需要共享对同一块内存的管理权时。

    #include <memory>
    #include <iostream>
    
    std::shared_ptr<int> sp1 = std::make_shared<int>(200);
    std::cout << "Count: " << sp1.use_count() << std::endl; // 输出 1
    
    std::shared_ptr<int> sp2 = sp1; // 复制,引用计数增加
    std::cout << "Count: " << sp1.use_count() << std::endl; // 输出 2
    
    // 当 sp1 和 sp2 都离开作用域时,内存才会被释放
    
  3. std::weak_ptr (弱指针)

    • 特性: 一种不控制对象生命周期的智能指针,它指向一个由 shared_ptr管理的对象。它不增加引用计数。

    • 用途: 主要用于解决 shared_ptr 可能导致的循环引用问题。

建议: 在现代 C++ 编程中,应优先使用智能指针,仅在必要时(如与C库交互)才使用原始指针。这能极大地提高代码的安全性和健壮性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值