MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++常指针与指针常量的混淆点辨析

2021-03-092.6k 阅读

C++ 中的指针基础回顾

在深入探讨常指针与指针常量的混淆点之前,我们先来简单回顾一下 C++ 中指针的基础知识。指针是一种特殊类型的变量,它存储的是另一个变量的内存地址。通过指针,我们可以间接访问和修改该内存地址处的值。

例如,下面这段简单的代码展示了如何定义一个指针并使用它:

#include <iostream>
int main() {
    int num = 10;
    int* ptr = &num; // 定义一个指向 int 类型变量 num 的指针 ptr
    std::cout << "The value of num is: " << *ptr << std::endl; // 通过指针访问 num 的值
    *ptr = 20; // 通过指针修改 num 的值
    std::cout << "The new value of num is: " << num << std::endl;
    return 0;
}

在上述代码中,int* ptr = &num; 定义了一个名为 ptr 的指针,它指向 num 的内存地址。*ptr 表示通过指针 ptr 间接访问其所指向的变量的值。

常指针(Pointer to const)

定义与特性

常指针,也就是指向常量的指针,意味着指针所指向的值不能通过该指针进行修改,但指针本身可以指向其他地址。其定义方式如下:

const int* ptr;

这里,ptr 是一个指向 const int 类型的指针。也就是说,不能通过 ptr 来修改它所指向的 int 类型的值。例如:

#include <iostream>
int main() {
    int num1 = 10;
    const int* ptr = &num1;
    // *ptr = 20;  // 这行代码会报错,因为不能通过常指针修改其指向的值
    int num2 = 20;
    ptr = &num2; // 可以让指针指向其他地址
    std::cout << "The value ptr points to is: " << *ptr << std::endl;
    return 0;
}

在上述代码中,如果尝试取消注释 *ptr = 20; 这行代码,编译器会报错,提示不能通过常指针修改其指向的值。但可以让 ptr 指向其他 int 类型的变量,如 num2

应用场景

常指针在很多场景下都非常有用,尤其是当我们希望确保某个函数不会修改传入的对象的值时。例如,考虑一个函数 printValue,它只负责打印传入的 int 类型的值,而不应该修改它:

#include <iostream>
void printValue(const int* value) {
    std::cout << "The value is: " << *value << std::endl;
    // *value = 100; // 这行代码会报错,符合预期,函数不应修改传入的值
}
int main() {
    int num = 50;
    printValue(&num);
    return 0;
}

printValue 函数中,使用常指针 const int* value 作为参数,这样函数内部就无法通过 value 指针修改传入的 int 值,从而保证了数据的安全性。

与普通指针的区别

与普通指针相比,常指针的关键区别在于对所指向值的修改限制。普通指针可以随意修改其指向的值,而常指针不行。例如:

#include <iostream>
int main() {
    int num1 = 10;
    int* normalPtr = &num1;
    const int* constPtr = &num1;
    *normalPtr = 20; // 合法,普通指针可以修改其指向的值
    // *constPtr = 30; // 非法,常指针不能修改其指向的值
    return 0;
}

此外,普通指针可以指向非常量或常量对象,而常指针只能指向常量对象(从类型安全性角度考虑)。例如:

#include <iostream>
int main() {
    int num = 10;
    const int constNum = 20;
    int* normalPtr = &num;
    normalPtr = &constNum; // 合法,普通指针可以指向常量对象
    const int* constPtr = &num; // 合法,常指针可以指向非常量对象,但不能通过它修改值
    // constPtr = &num; // 合法,常指针可以重新指向其他非常量对象
    return 0;
}

指针常量(Constant Pointer)

定义与特性

指针常量,即常量指针,意味着指针本身是一个常量,一旦初始化后,它就不能再指向其他地址,但它所指向的值是可以修改的。其定义方式如下:

int* const ptr;

这里,ptr 是一个 const 类型的指针,它只能指向初始化时指定的地址。例如:

#include <iostream>
int main() {
    int num1 = 10;
    int* const ptr = &num1;
    *ptr = 20; // 合法,可以通过指针常量修改其指向的值
    int num2 = 30;
    // ptr = &num2; // 这行代码会报错,指针常量不能再指向其他地址
    std::cout << "The value ptr points to is: " << *ptr << std::endl;
    return 0;
}

在上述代码中,*ptr = 20; 是合法的,因为指针常量所指向的值可以修改。但如果尝试取消注释 ptr = &num2; 这行代码,编译器会报错,因为指针常量不能再指向其他地址。

应用场景

指针常量常用于一些需要固定指向某个对象的场景,比如在实现一个单例模式时。单例模式确保一个类只有一个实例,并提供一个全局访问点。在实现过程中,可以使用指针常量来指向唯一的实例对象:

class Singleton {
private:
    static Singleton* const instance;
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
public:
    static Singleton* getInstance() {
        return instance;
    }
};
Singleton* const Singleton::instance = new Singleton();
int main() {
    Singleton* s1 = Singleton::getInstance();
    Singleton* s2 = Singleton::getInstance();
    std::cout << (s1 == s2) << std::endl; // 输出 1,表示 s1 和 s2 指向同一个对象
    return 0;
}

在上述代码中,Singleton* const instance 定义了一个指针常量 instance,它始终指向 Singleton 类的唯一实例。这样可以保证在整个程序运行期间,instance 不会指向其他对象,从而实现单例模式。

与常指针的区别

指针常量与常指针的区别主要体现在对指针本身和所指向值的可修改性上。常指针不能修改其指向的值,但指针本身可以重新赋值指向其他地址;而指针常量不能重新赋值指向其他地址,但可以修改其指向的值。例如:

#include <iostream>
int main() {
    int num1 = 10;
    int num2 = 20;
    const int* constPtr = &num1;
    int* const ptrConst = &num1;
    // *constPtr = 30; // 非法,常指针不能修改其指向的值
    constPtr = &num2; // 合法,常指针可以重新指向其他地址
    *ptrConst = 30; // 合法,指针常量可以修改其指向的值
    // ptrConst = &num2; // 非法,指针常量不能重新指向其他地址
    return 0;
}

此外,在定义时,常指针是将 const 放在类型前面,而指针常量是将 const 放在指针标识符后面。这种语法上的差异也是区分它们的重要标志。

混淆点详细辨析

语法混淆

在实际编程中,常指针和指针常量最常见的混淆点之一就是语法。由于它们的定义语法较为相似,很容易写错。例如,以下两种定义很容易混淆:

const int* ptr1; // 常指针,指向常量的指针
int* const ptr2; // 指针常量,常量指针

要正确区分它们,可以记住一个简单的规则:const 修饰谁,谁就不能被修改。对于 const int* ptr1const 修饰的是 int,即 ptr1 所指向的值不能通过 ptr1 修改,所以 ptr1 是常指针;对于 int* const ptr2const 修饰的是 ptr2,即 ptr2 本身不能再指向其他地址,所以 ptr2 是指针常量。

另外,在复杂的声明中,这种混淆可能会更加明显。例如:

const int* const ptr3;

这里,ptr3 既是一个常指针(因为 const 修饰了 int,所指向的值不能通过 ptr3 修改),又是一个指针常量(因为 const 修饰了 ptr3ptr3 本身不能再指向其他地址)。

语义混淆

除了语法混淆,语义上也容易产生混淆。程序员可能会错误地理解常指针和指针常量的含义,导致在代码中使用不当。例如,可能会错误地认为常指针是指针本身不能改变,而指针常量是所指向的值不能改变,这与它们的实际定义正好相反。

为了避免语义混淆,在编写代码时,要明确自己的意图。如果希望保护所指向的值不被修改,就使用常指针;如果希望指针固定指向某个对象,不允许重新赋值,就使用指针常量。例如,在一个函数中,如果传入的参数是一个对象的指针,并且函数不应该修改该对象的值,那么应该使用常指针作为参数类型:

#include <iostream>
void processData(const int* data) {
    // 函数内不能通过 data 修改其指向的值
    std::cout << "The data value is: " << *data << std::endl;
}
int main() {
    int num = 42;
    processData(&num);
    return 0;
}

而如果在一个类中,有一个成员指针需要固定指向某个内部对象,并且在对象的生命周期内不应该改变指向,那么应该使用指针常量作为成员变量:

class MyClass {
private:
    int data;
    int* const ptr;
public:
    MyClass() : data(0), ptr(&data) {}
    void setData(int value) {
        *ptr = value; // 可以通过指针常量修改其指向的值
    }
    int getData() const {
        return *ptr;
    }
};
int main() {
    MyClass obj;
    obj.setData(10);
    std::cout << "The data in MyClass is: " << obj.getData() << std::endl;
    return 0;
}

初始化混淆

常指针和指针常量在初始化时也可能会产生混淆。常指针在定义时可以不初始化,但指针常量必须在定义时进行初始化,因为它一旦定义后就不能再改变指向。例如:

const int* ptr1; // 合法,常指针可以不初始化
int* const ptr2; // 非法,指针常量必须在定义时初始化
int num = 10;
int* const ptr3 = &num; // 合法,指针常量在定义时初始化

如果忘记对指针常量进行初始化,编译器会报错。另外,在初始化常指针时,虽然可以指向非常量对象,但要注意不能通过常指针修改该对象的值。例如:

int num1 = 10;
const int* ptr = &num1;
// *ptr = 20; // 非法,不能通过常指针修改其指向的值

而对于指针常量,一旦初始化后,即使所指向的对象的值发生了变化,指针常量仍然指向该对象。例如:

int num1 = 10;
int* const ptr = &num1;
num1 = 20; // 合法,所指向的值可以改变
// ptr = &anotherNum; // 非法,指针常量不能再指向其他地址

综合示例与实际应用

综合示例代码

下面通过一个综合示例代码来进一步展示常指针和指针常量的用法以及它们的区别:

#include <iostream>
void printValue(const int* value) {
    std::cout << "The value is: " << *value << std::endl;
}
class MyClass {
private:
    int data;
    int* const ptr;
public:
    MyClass() : data(0), ptr(&data) {}
    void setData(int value) {
        *ptr = value;
    }
    int getData() const {
        return *ptr;
    }
};
int main() {
    int num1 = 10;
    int num2 = 20;
    const int* constPtr = &num1;
    int* const ptrConst = &num1;
    // *constPtr = 30; // 非法,常指针不能修改其指向的值
    constPtr = &num2; // 合法,常指针可以重新指向其他地址
    *ptrConst = 30; // 合法,指针常量可以修改其指向的值
    // ptrConst = &num2; // 非法,指针常量不能重新指向其他地址
    printValue(constPtr);
    MyClass obj;
    obj.setData(42);
    std::cout << "The data in MyClass is: " << obj.getData() << std::endl;
    return 0;
}

在上述代码中,首先定义了一个 printValue 函数,它接受一个常指针作为参数,用于打印值。然后定义了 MyClass 类,其中包含一个指针常量 ptr,用于指向类内部的 data 成员变量。在 main 函数中,展示了常指针和指针常量的各种操作,包括初始化、修改值、重新指向等,并调用了 printValue 函数和 MyClass 类的相关方法。

在实际项目中的应用

在实际的 C++ 项目中,常指针和指针常量有着广泛的应用。例如,在大型的图形渲染引擎中,当传递一些只读的数据(如顶点数据、纹理数据等)给渲染函数时,常常使用常指针,以确保这些数据在传递过程中不会被意外修改。

class VertexBuffer {
private:
    float* data;
    int size;
public:
    VertexBuffer(float* vertexData, int dataSize) : data(vertexData), size(dataSize) {}
    void render() const {
        // 渲染函数,使用常指针确保数据不被修改
        const float* ptr = data;
        for (int i = 0; i < size; ++i) {
            std::cout << "Vertex data: " << *ptr << std::endl;
            ++ptr;
        }
    }
};
int main() {
    float vertices[] = {1.0f, 2.0f, 3.0f, 4.0f};
    VertexBuffer vb(vertices, 4);
    vb.render();
    return 0;
}

在上述代码中,render 函数使用常指针 const float* ptr 来遍历顶点数据,这样可以保证在渲染过程中顶点数据不会被修改。

而指针常量在实现一些资源管理类时非常有用。例如,一个文件管理类可能会使用指针常量来指向一个打开的文件句柄,确保在文件对象的生命周期内,文件句柄不会被意外修改指向其他文件。

#include <iostream>
#include <fstream>
class FileManager {
private:
    std::ifstream* const file;
public:
    FileManager(const char* filename) : file(new std::ifstream(filename)) {
        if (!file->is_open()) {
            std::cerr << "Failed to open file" << std::endl;
        }
    }
    ~FileManager() {
        if (file->is_open()) {
            file->close();
            delete file;
        }
    }
    void readFile() {
        std::string line;
        while (std::getline(*file, line)) {
            std::cout << line << std::endl;
        }
    }
};
int main() {
    FileManager fm("test.txt");
    fm.readFile();
    return 0;
}

在上述代码中,FileManager 类的 file 成员变量是一个指针常量,它在构造函数中被初始化并指向打开的文件。在文件管理类的生命周期内,file 始终指向同一个文件,保证了文件操作的一致性。

通过以上详细的介绍、示例代码以及实际应用场景的分析,相信大家对 C++ 中常指针与指针常量的混淆点有了更清晰的认识和理解。在实际编程中,正确使用常指针和指针常量可以提高代码的安全性、可读性和可维护性。