从头开始c++ day4

C++ 对象模型与底层机制:this 指针、友元与访问控制全解析

在学习 C++ 类与对象的过程中,你可能会产生这样的疑问:一个类的多个对象,为何能共享成员函数却拥有各自的成员变量?调用成员函数时,编译器如何知道操作的是哪个对象的成员变量? 这些问题的答案,藏在 C++ 的对象模型this 指针中。

对象模型决定了类的成员在内存中如何存储,而 this 指针是连接对象与成员函数的 "隐形桥梁"。本文将从底层存储机制出发,详解成员变量与成员函数的分开存储特性、this 指针的概念与用法、空指针访问成员函数的坑,以及 const 修饰成员函数的核心逻辑,带你彻底看透 C++ 对象的底层实现。

一、成员变量和成员函数的分开存储:对象的内存布局

C++ 的对象模型有一个核心设计:类的成员变量和成员函数在内存中是分开存储的。这一设计的目的是为了节省内存空间,让多个对象可以共享同一份成员函数的代码。

1. 内存存储的核心规则

  • 成员变量:属于具体的对象,每个对象都有独立的成员变量副本,存储在栈区 / 堆区(根据对象的创建方式);
  • 成员函数 :不属于任何对象,所有对象共享同一份成员函数代码,存储在代码区(常量区)
  • 空类的大小 :一个不包含任何成员的空类,其对象的大小为1 字节(这 1 字节是编译器为了标识对象的内存地址而分配的占位符,无实际数据意义)。

2. 代码验证:对象的内存大小计算

通过sizeof运算符可以直观看到对象的内存布局,验证成员变量和成员函数的分开存储特性。

复制代码
#include <iostream>
using namespace std;

// 空类
class Empty {
};

// 包含成员变量和成员函数的类
class Person {
private:
    // 成员变量:int占4字节,string占24字节(不同编译器可能略有差异)
    int age;
    string name;

public:
    // 成员函数:存储在代码区,不占用对象内存
    void showInfo() {
        cout << "姓名:" << name << ",年龄:" << age << endl;
    }

    void setInfo(string s_name, int s_age) {
        name = s_name;
        age = s_age;
    }
};

int main() {
    // 空类对象的大小:1字节(占位符)
    Empty e;
    cout << "空类对象的大小:" << sizeof(e) << "字节\n";

    // Person类对象的大小:仅等于成员变量的总大小(4+24=28字节)
    Person p;
    cout << "Person对象的大小:" << sizeof(p) << "字节\n";

    return 0;
}

运行结果

复制代码
空类对象的大小:1字节
Person对象的大小:28字节

从结果可以看出:Person 对象的大小仅由成员变量agename的大小决定,成员函数showInfosetInfo并未占用对象的内存空间,所有 Person 对象都会共享这两个函数的代码。

3. 设计优势:节省内存空间

如果成员函数也属于每个对象,那么创建 1000 个 Person 对象就会有 1000 份相同的成员函数代码,造成巨大的内存浪费。而分开存储的设计,让无论多少个对象都只共享一份成员函数代码,仅为每个对象分配独立的成员变量内存,极大地节省了内存资源。

二、this 指针:连接对象与成员函数的 "隐形桥梁"

既然所有对象共享同一份成员函数,那么当调用p1.showInfo()p2.showInfo()时,成员函数如何知道要操作p1还是p2的成员变量?答案就是this 指针

1. this 指针的概念与特性

this 指针是 C++ 编译器为每个非静态成员函数 隐含的一个参数,它是一个指向当前对象的常量指针 ,其本质是类名* const this

核心特性

  • this 指针由编译器自动传递给成员函数,无需开发者手动定义和传递;
  • this 指针指向当前调用成员函数的对象,成员函数通过 this 指针访问当前对象的成员变量;
  • this 指针存储在栈区(部分编译器会优化到寄存器中),生命周期随成员函数的调用结束而销毁;
  • 静态成员函数中没有 this 指针(因为静态成员函数属于类,不属于具体对象)。

2. this 指针的显式使用场景

通常情况下,this 指针是隐含使用的,编译器会自动帮我们通过 this 指针访问成员变量。但在某些场景下,需要显式使用 this 指针:

场景 1:解决成员变量与函数参数的命名冲突

当成员变量的名字与函数参数的名字相同时,通过 this 指针可以明确区分。

复制代码
#include <iostream>
#include <string>
using namespace std;

class Person {
private:
    string name;
    int age;

public:
    // 参数名与成员变量名相同,通过this指针区分
    void setInfo(string name, int age) {
        this->name = name; // this->name 指向当前对象的name成员
        this->age = age;   // this->age 指向当前对象的age成员
    }

    void showInfo() {
        // 隐含使用this指针:this->name、this->age
        cout << "姓名:" << name << ",年龄:" << age << endl;
    }
};

int main() {
    Person p;
    p.setInfo("张三", 20);
    p.showInfo(); // 输出:姓名:张三,年龄:20
    return 0;
}
场景 2:返回当前对象本身(链式编程)

在成员函数中返回*this(解引用 this 指针,得到当前对象),可以实现链式调用,让代码更简洁。

复制代码
#include <iostream>
#include <string>
using namespace std;

class Person {
private:
    int age;

public:
    Person& addAge(int num) { // 返回对象的引用(避免拷贝)
        age += num;
        return *this; // 返回当前对象本身
    }

    void showAge() {
        cout << "年龄:" << age << endl;
    }
};

int main() {
    Person p;
    // 链式调用:连续调用addAge函数
    p.addAge(5).addAge(10).addAge(5);
    p.showAge(); // 输出:年龄:20
    return 0;
}

注意 :如果返回值是Person(值返回),会创建临时对象,链式调用的是临时对象的成员函数,而非原对象;返回Person&(引用返回)才会操作原对象。

三、空指针访问成员函数:危险的边界情况

空指针是指向NULL的指针,代表不指向任何有效的对象。那么,通过空指针调用类的成员函数会发生什么?答案是分情况而定

1. 空指针调用不访问成员变量的成员函数:可以执行

如果成员函数中没有通过 this 指针访问成员变量,那么即使通过空指针调用,函数也能正常执行 ------ 因为成员函数存储在代码区,与对象无关,此时 this 指针为NULL,但并未被解引用。

复制代码
#include <iostream>
#include <string>
using namespace std;

class Person {
public:
    void showHello() {
        // 没有访问任何成员变量,仅执行打印逻辑
        cout << "Hello, C++\n";
    }
};

int main() {
    // 定义空指针
    Person* p = NULL;
    // 通过空指针调用showHello:正常执行
    p->showHello();
    return 0;
}

运行结果

复制代码
Hello, C++

2. 空指针调用访问成员变量的成员函数:程序崩溃

如果成员函数中通过 this 指针访问了成员变量,那么通过空指针调用时,程序会直接崩溃 ------ 因为此时 this 指针为NULL,解引用NULL指针是非法操作。

复制代码
#include <iostream>
#include <string>
using namespace std;

class Person {
private:
    string name;

public:
    void setName(string name) {
        // this指针为NULL,解引用this->name会导致程序崩溃
        this->name = name;
    }
};

int main() {
    Person* p = NULL;
    // 通过空指针调用setName:程序崩溃
    p->setName("张三");
    return 0;
}

3. 避坑技巧:在成员函数中判空

为了避免空指针调用成员函数导致的崩溃,可以在成员函数的开头对 this 指针进行判空处理,提前终止函数执行。

复制代码
#include <iostream>
#include <string>
using namespace std;

class Person {
private:
    string name;

public:
    void setName(string name) {
        // 对this指针判空,避免空指针解引用
        if (this == NULL) {
            cout << "错误:对象指针为空,无法设置姓名\n";
            return;
        }
        this->name = name;
    }
};

int main() {
    Person* p = NULL;
    p->setName("张三"); // 输出:错误:对象指针为空,无法设置姓名
    return 0;
}

四、const 修饰成员函数:常函数与常对象

在成员函数的末尾添加const关键字,就得到了常函数。const 的作用是修饰 this 指针,限制成员函数对成员变量的修改,这是 C++ 中保证数据安全性的重要机制。

1. 常函数的语法与核心原理

语法返回值类型 函数名(参数列表) const { ... }

核心原理

  • 普通成员函数的 this 指针类型是类名* const this(指针本身是常量,指向的对象可以修改);
  • 常函数的 this 指针类型是const 类名* const this(指针本身是常量,指向的对象也被 const 修饰,无法修改)。

因此,常函数中不能修改普通的成员变量,但可以访问成员变量。

2. 常函数的代码示例

复制代码
#include <iostream>
#include <string>
using namespace std;

class Person {
private:
    string name;
    int age;

public:
    Person(string name, int age) : name(name), age(age) {}

    // 常函数:不能修改成员变量
    void showInfo() const {
        // 错误:常函数中不能修改成员变量
        // age = 25;
        // name = "李四";

        // 正确:可以访问成员变量
        cout << "姓名:" << name << ",年龄:" << age << endl;
    }

    // 普通成员函数:可以修改成员变量
    void setAge(int age) {
        this->age = age;
    }
};

int main() {
    Person p("张三", 20);
    p.showInfo(); // 输出:姓名:张三,年龄:20
    return 0;
}

3. mutable 关键字:突破常函数的修改限制

如果希望某个成员变量在常函数中也能被修改,可以用mutable关键字修饰该成员变量。mutable的含义是 "可变的",它会让被修饰的成员变量不受 const 的限制。

复制代码
#include <iostream>
#include <string>
using namespace std;

class Person {
private:
    string name;
    mutable int age; // mutable修饰,常函数中可修改

public:
    Person(string name, int age) : name(name), age(age) {}

    // 常函数:可以修改mutable修饰的age
    void changeAge(int newAge) const {
        age = newAge;
        // 错误:name未被mutable修饰,仍不能修改
        // name = "李四";
    }

    void showInfo() const {
        cout << "姓名:" << name << ",年龄:" << age << endl;
    }
};

int main() {
    Person p("张三", 20);
    p.changeAge(25);
    p.showInfo(); // 输出:姓名:张三,年龄:25
    return 0;
}

4. 常对象:只能调用常函数的对象

const修饰的对象称为常对象,常对象有两个核心特性:

  • 常对象的成员变量不能被修改(除非被mutable修饰);

  • 常对象只能调用常函数,不能调用普通成员函数(避免普通成员函数修改成员变量)。

    #include <iostream>
    #include <string>
    using namespace std;

    class Person {
    private:
    string name;
    mutable int age;

    public:
    Person(string name, int age) : name(name), age(age) {}

    复制代码
      void showInfo() const {
          cout << "姓名:" << name << ",年龄:" << age << endl;
      }
    
      void setName(string newName) {
          name = newName;
      }

    };

    int main() {
    // 定义常对象
    const Person p("张三", 20);
    p.showInfo(); // 正确:常对象调用常函数

    复制代码
      // 错误:常对象不能调用普通成员函数
      // p.setName("李四");
    
      return 0;

    }

五、友元:打破封装的灵活访问者

友元机制允许外部函数 / 类访问类的私有成员,是 "封装性" 与 "灵活性" 的折中方案。

1. 全局函数做友元

语法friend 返回值类型 函数名(参数);示例:全局函数访问类的私有成员

复制代码
class Building {
    friend void visit(Building& b); // 声明友元
private:
    string bedroom = "卧室";
public:
    string sittingRoom = "客厅";
};

void visit(Building& b) {
    cout << b.sittingRoom << endl;
    cout << b.bedroom << endl; // 友元函数可访问私有成员
}

2. 类做友元

语法friend class 友元类名;示例:友元类的所有成员函数访问私有成员

复制代码
class Building; // 提前声明

class GoodGay {
public:
    void visit(Building& b);
};

class Building {
    friend class GoodGay; // 声明GoodGay为友元类
private:
    string bedroom = "卧室";
public:
    string sittingRoom = "客厅";
};

void GoodGay::visit(Building& b) {
    cout << b.sittingRoom << endl;
    cout << b.bedroom << endl;
}

3. 成员函数做友元

语法friend 返回值类型 友元类名::成员函数名(参数);示例:仅单个成员函数访问私有成员

复制代码
class Building;

class GoodGay {
public:
    void visitBedroom(Building& b);
    void visitSittingRoom(Building& b);
};

class Building {
    friend void GoodGay::visitBedroom(Building& b); // 仅声明单个友元函数
private:
    string bedroom = "卧室";
public:
    string sittingRoom = "客厅";
};

void GoodGay::visitBedroom(Building& b) {
    cout << b.bedroom << endl; // 可访问私有成员
}

void GoodGay::visitSittingRoom(Building& b) {
    // cout << b.bedroom << endl; // 错误:非友元函数不可访问
    cout << b.sittingRoom << endl;
}

友元的注意事项

  • 友元不具备传递性AB的友元,BC的友元,A不是C的友元;
  • 友元单向生效A声明B为友元,B不自动成为A的友元;
  • 优先使用成员函数做友元:仅开放单个函数的访问权,封装性破坏最小。

六、常见误区与注意事项

  1. 认为对象包含成员函数 :对象仅存储成员变量,成员函数存储在代码区被所有对象共享,sizeof(对象)的结果仅反映成员变量的大小;
  2. 滥用 this 指针:静态成员函数中没有 this 指针,不能通过 this 指针访问静态成员(直接访问即可);
  3. 空指针调用成员函数的侥幸心理:即使空指针能调用不访问成员变量的成员函数,也应避免这种写法,违反代码的可读性和安全性;
  4. 常函数的修改误区 :常函数中不能修改普通成员变量,但可以修改mutable修饰的成员变量,这是唯一的例外。

七、总结

C++ 的对象模型和 this 指针是理解类与对象底层实现的关键:

  • 成员变量和成员函数分开存储:对象仅持有成员变量,成员函数存储在代码区共享,这一设计极大地节省了内存空间;
  • this 指针:作为隐含参数连接对象与成员函数,解决了成员函数区分不同对象的问题,显式使用可解决命名冲突和实现链式编程;
  • 空指针访问成员函数:不访问成员变量时可执行,访问成员变量时会崩溃,建议在成员函数中对 this 指针判空;
  • const 修饰成员函数 :通过修饰 this 指针限制成员变量的修改,结合mutable可实现局部可变,常对象只能调用常函数,保证了数据的安全性。
  • 友元应谨慎使用,仅在 "必须打破封装才能解决问题" 的场景下采用,且优先选择 "成员函数做友元" 这种最精细的控制方式。

掌握这些知识点,你不仅能写出更高效、更安全的 C++ 代码,还能深入理解 C++ 面向对象的底层逻辑,为后续学习继承、多态、模板等高级特性打下坚实的基础。

相关推荐
~无忧花开~3 小时前
JavaScript实现PDF本地预览技巧
开发语言·前端·javascript
靠沿3 小时前
Java数据结构初阶——LinkedList
java·开发语言·数据结构
4***99743 小时前
Kotlin序列处理
android·开发语言·kotlin
froginwe113 小时前
Scala 提取器(Extractor)
开发语言
t***D2643 小时前
Kotlin在服务端开发中的生态建设
android·开发语言·kotlin
Elias不吃糖4 小时前
LeetCode每日一练(209, 167)
数据结构·c++·算法·leetcode
Want5954 小时前
C/C++跳动的爱心②
c语言·开发语言·c++
初晴や4 小时前
指针函数:从入门到精通
开发语言·c++
铁手飞鹰4 小时前
单链表(C语言,手撕)
数据结构·c++·算法·c·单链表