C++ 面向对象基础:类、访问权限,构造函数,析构函数

在从 C 语言迈向 C++ 的过程中,面向对象编程(OOP)是最核心的转变。在 C 语言中,我们习惯于用结构体(struct)来组织单纯的数据;但在 C++ 的世界里,"类(Class)"赋予了数据以生命。

今天,我将系统总结 C++ 中关于类的声明、权限控制、函数定义,并深度剖析构造函数与"初始化成员列表"的必用场景。

一、 类(Class)与 结构体(Struct)的核心区别

在 C++ 中,声明一个类与声明一个结构体非常相似(声明本身只是一个模板,不占用实际内存)。

⚠️ 经典误区纠正: 很多人以为"结构体只能存数据,类才能写函数"。其实在现代 C++ 中,结构体(struct)和类(class)都可以包含变量和函数 。它们之间唯一的本质区别在于默认的访问权限

  • struct :默认访问权限是 public(公开的)。

  • class :默认访问权限是 private(私有的)。

💻 代码验证

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

// C++ 中的 Struct,默认是 public
struct MyStruct {
    int age = 10;
    void print() { cout << "Struct 可以有函数!" << endl; } 
};

// C++ 中的 Class,默认是 private
class MyClass {
    int age = 20; // 默认是私有,外部不可见
    void print() { cout << "Class 也是私有!" << endl; } 
};

int main() {
    MyStruct s;
    s.print(); // ✅ 正常运行,外部可随意访问

    MyClass c;
    // c.print(); // ❌ 编译报错!因为 class 默认是私有的,外部无法调用
    return 0;
}

二、 三大访问修饰符

为了保证数据的安全性,C++ 提供了三种访问修饰符,用于精确控制数据和函数的访问级别:

  1. public(公有):声明的函数和变量,既可以在类内部访问,也可以在外部直接访问。

  2. private(私有):只能在类的内部(类的成员函数中)访问,外部绝对不可见。这完美体现了面向对象的"封装"特性。

  3. protected(受保护) :在外部不可访问,但允许在派生类(子类)中被访问。主要用于继承机制。

💻 代码示例:

复制代码
class BankAccount {
private:
    int password; // 核心密码:只能内部修改,外部绝对看不到

protected:
    int balance;  // 账户余额:外部不能碰,但以后的"子类卡"可以碰

public:
    // 公开接口:提供给外部操作的合法途径
    void setPassword(int p) {
        password = p; // 内部函数可以自由访问私有变量
    }
};

int main() {
    BankAccount myCard;
    myCard.setPassword(123456); // ✅ 正常,调用公开函数
    // myCard.password = 123456; // ❌ 报错!私有变量不能直接修改
    return 0;
}

三、 成员函数的类内与类外定义

类内部声明的函数有两种实现方式,根据代码长短和工程习惯,我们可以灵活选择:

  1. 类内定义:直接在类的花括号内写完函数体。适合简短的函数。

  2. 类外定义 :类内部只写声明,外部写实现。此时函数名前必须加上作用域解析符 类名::。适合复杂的函数,让类的结构一目了然。

💻 代码示例:

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

class Dog {
public:
    // 1. 类内定义
    void eat() { 
        cout << "吃骨头" << endl; 
    }
    
    // 2. 类外定义的第一步:内部只写声明
    void bark(); 
};

// 2. 类外定义的第二步:外部写实现,必须加 Dog::
void Dog::bark() {
    cout << "汪汪汪!" << endl;
}

int main() {
    Dog dog1;
    dog1.eat();
    dog1.bark();
    return 0;
}

四、 构造函数(Constructor)与函数重载

当类被创建(实例化)时,系统会自动执行一个特殊的函数------构造函数。它的主要任务是:在对象分配内存后,立刻为类中的变量进行初始化。

构造函数的铁律:

  • 名字与类名完全相同。

  • 没有返回值(连 void 都不能写)。

  • 默认生成:如果你不写,编译器会自动生成一个隐藏的"无参构造函数"。

  • 函数重载:构造函数支持重载。你可以写多个同名构造函数,通过传入不同个数或类型的参数,编译器会自动匹配。

💻 代码示例:

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

class Student {
public:
    string name;
    
    // 构造函数重载 1:无参
    Student() { 
        name = "未知"; 
        cout << "调用了无参构造函数" << endl;
    }
    
    // 构造函数重载 2:有参
    Student(string n) { 
        name = n; 
        cout << "调用了有参构造函数,姓名:" << name << endl;
    }
};

int main() {
    Student s1;        // 自动调用无参构造函数
    Student s2("Tom"); // 自动匹配并调用有参构造函数
    return 0;
}

五、 必须使用"初始化成员列表"的 3 种极限场景

在普通的构造函数内部,我们可以通过 = 为普通变量赋值。但在以下三种特殊情况下,必须使用"初始化成员列表"进行赋值:

  1. 常量(const 修饰的变量)

  2. 引用(& 修饰的别名)

    原因:常量和引用的底层特性决定了,它们必须在内存分配的那一瞬间就完成初始化,绝不允许"先分配内存,事后再赋值"。如果写在构造函数体内用 = 赋值,编译器会直接报错。

  3. 嵌套类对象(且该类没有无参构造函数)

    原因:如果在类 A 中嵌套了类 B 的对象,而类 B 只定义了带参数的构造函数(这会导致其默认的无参构造函数失效)。当创建类 A 时,编译器虽然知道该分配多少内存,但不知道该传什么参数去初始化类 B。此时必须在 A 的初始化列表中显式调用 B 的构造函数。

格式: 类名(形参) : 成员1(值), 成员2(值) {}

💻 终极代码示例:

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

// 被嵌套的类 B
class Engine {
public:
    int power;
    // 注意:这里定义了带参构造,默认的无参构造就失效了!
    Engine(int p) { power = p; } 
};

// 类 A
class Car {
private:
    const int wheels;  // 极限场景 1:常量
    int& ref_id;       // 极限场景 2:引用
    Engine myEngine;   // 极限场景 3:嵌套了没有无参构造函数的类

public:
    // 必须使用冒号引出的"初始化成员列表"!
    Car(int w, int& id, int p) : wheels(w), ref_id(id), myEngine(p) 
    {
        // 这里是普通的构造函数体
        cout << "汽车制造完毕!" << endl;
        cout << "轮子数量:" << wheels << endl;
        cout << "马力:" << myEngine.power << endl;
        cout << "出厂编号:" << ref_id << endl;
    }
};

int main() {
    int id = 888;
    // 创建类时,传入对应参数
    Car myCar(4, id, 500); 
    return 0;
}

六、 析构函数(Destructor):对象的"临终遗言"

如果说构造函数是对象诞生时的"出生证明",那么析构函数就是对象生命周期结束时的"临终遗言"。当一个对象的作用域结束被销毁时,系统会自动调用析构函数。

析构函数的 4 条铁律:

  1. 命名规则 :名字必须是 波浪号 ~ 加上类名 (例如 ~Student())。

  2. 无形参、无返回 :它没有任何参数,连 void 都不能写。

  3. 绝对不能重载 :因为没有参数,所以一个类只能有一个析构函数。

  4. 主要使命 :清理战场。如果你的对象在活着的时候向系统借了资源(比如用 new 在堆区申请了内存、打开了某个文件等),必须 在析构函数里把它们还回去(用 delete 释放内存或关闭文件),否则就会造成内存泄漏

提示: 和构造函数一样,如果你不写析构函数,编译器也会在后台默默生成一个什么都不做的"默认析构函数"。但只要你用了指针和动态内存,就必须手动写析构函数。

💻 终极代码示例(构造与析构的完美配合):

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

class Computer {
private:
    char* cpu_name; // 一个指针,准备指向动态分配的内存

public:
    // 1. 构造函数:对象创建时自动运行
    Computer(const char* name) {
        // 使用 C++ 的 new 关键字,在堆区申请一块内存
        cpu_name = new char[strlen(name) + 1]; 
        strcpy(cpu_name, name);
        cout << "【诞生】构造函数被调用,电脑组装完毕!CPU: " << cpu_name << endl;
    }

    // 2. 析构函数:对象销毁时自动运行
    ~Computer() {
        // 对象死前,必须用 delete[] 把 new 出来的内存还给系统
        delete[] cpu_name; 
        cout << "【死亡】析构函数被调用,电脑报废,内存已成功回收!" << endl;
    }
};

void testFunction() {
    cout << "--- 进入测试函数 ---" << endl;
    Computer myPc("Intel Core i9"); // 此时对象被创建,调用构造函数
    cout << "--- 准备离开测试函数 ---" << endl;
} // 函数执行到这个右大括号时,myPc 对象的寿命结束,自动调用析构函数!

int main() {
    testFunction();
    cout << "程序即将彻底结束。" << endl;
    return 0;
}
相关推荐
t***5441 小时前
如何在现代C++项目中有效应用这些设计模式
开发语言·c++·设计模式
野生技术架构师1 小时前
2026年Java面试题集锦(含答案)
java·开发语言·面试
lolo大魔王1 小时前
Go语言的defer语句和Test功能测试函数
开发语言·后端·golang
kyle~1 小时前
导航---LIO(激光雷达-惯性里程计)算法
c++·算法·机器人·ros2·导航
杰众物联1 小时前
超高频FPC标签企业
c++
无限进步_2 小时前
【C++】私有虚函数与多态:访问权限不影响动态绑定
开发语言·c++·ide·windows·git·算法·visual studio
努力努力再努力wz2 小时前
【MySQL入门系列】:不只是建表:MySQL 表约束与 DDL 执行机制全解析
android·linux·服务器·数据结构·数据库·c++·mysql
lolo大魔王2 小时前
Go语言的结构体
开发语言·后端·golang
lly2024062 小时前
C 作用域规则
开发语言