一、类的定义
1. 类的基本形式
class 类名 {
public: // 公有成员(类内外均可访问)数据类型 数据成员; // 公有数据成员数据类型 成员函数(参数列表); // 公有成员函数声明
protected: // 保护成员(类内和派生类可访问)数据类型 保护数据成员;数据类型 保护成员函数(参数列表);
private: // 私有成员(仅类内可访问)数据类型 私有数据成员;数据类型 成员函数(参数列表); // 私有成员函数声明
}; // 类定义结束必须加分号
2. 详细说明
- 成员分类:类包含两种成员:
- 数据成员:描述类的属性,如学生类中的
姓名
、年龄
。 - 成员函数:描述类的行为,如学生类中的
设置年龄
、获取成绩
。
- 数据成员:描述类的属性,如学生类中的
- 访问权限:
public
(公有):成员可在类外直接访问,例如对象名.公有成员
。protected
(保护):成员可在类内和派生类中访问,类外不可直接访问。private
(私有):仅类内成员函数或友元可访问,类外和派生类(除非友元)不可直接访问。
- 成员函数定义:
- 类内定义:直接在类体中编写函数体,自动成为内联函数(隐式
inline
)。 - 类外定义:使用
类名::
作用域限定符,例如:void 类名::成员函数(参数列表) { /* 函数体 */ }
- 类内定义:直接在类体中编写函数体,自动成为内联函数(隐式
3. 示例:定义学生类
class Student {
public:// 公有数据成员char name[20]; // 姓名int age; // 年龄// 公有成员函数(类内定义)void set_score(float s) { // 设置成绩(私有成员)score = s;}// 公有成员函数(类外定义)float get_score(); // 获取成绩声明protected:int grade; // 年级(保护成员,派生类可访问)private:float score; // 成绩(私有成员,仅类内访问)
};// 类外定义成员函数
float Student::get_score() {return score; // 访问私有成员score
}
二、对象的定义与成员访问
1. 对象定义
- 语法:
类名 对象名;
或类名 对象名(参数列表);
(调用带参构造函数) - 示例:
Student stu1; // 定义对象stu1(调用默认构造函数) Student stu2("张三", 18); // 假设存在带参构造函数
2. 成员访问
- 公有成员:直接通过
对象名.成员名
访问。stu1.age = 18; // 合法,age是公有成员 strcpy(stu1.name, "李四"); // 合法
- 私有 / 保护成员:通过公有成员函数间接访问。
stu1.set_score(90.5); // 合法,调用公有函数设置私有成员score // stu1.score = 90.5; // 非法,score是私有成员
三、构造函数:对象的初始化
在面向对象编程中,对象的初始化是一个关键环节。构造函数作为类的特殊成员函数,承担着初始化对象的重要职责。下面将从基础概念到高级用法逐步解析构造函数的核心知识。
1. 构造函数概述
语法规则
- 函数名:必须与类名完全相同(包括大小写)。
- 返回值:没有返回值(不能写
void
)。 - 调用时机:创建对象时自动调用(栈上定义、堆上
new
操作、函数返回对象等场景)。
核心作用
- 成员初始化:为对象的成员变量赋初始值(如数值类型设为默认值、指针指向有效内存)。
- 资源分配:为对象分配运行所需资源(如动态内存、文件句柄、网络连接等)。
示例:基础构造函数
class Book {
public:// 构造函数:初始化书名和页数Book() {strcpy(title, "未命名"); // 初始化C风格字符串pages = 0;}
private:char title[50];int pages;
};int main() {Book novel; // 创建对象时自动调用Book()构造函数return 0;
}
2. 构造函数分类
(1)无参构造函数(默认构造函数)
定义与特性
- 无参数:函数括号内没有参数列表。
- 编译器自动生成:若类中未定义任何构造函数,编译器会生成一个空的默认构造函数(成员变量初始化为默认值,如数值为 0、指针为
nullptr
)。 - 手动定义的必要性:若定义了其他带参构造函数,编译器不再自动生成默认构造函数,需手动定义以支持无参创建对象。
示例:手动定义默认构造函数
class Student {
public:// 显式定义默认构造函数Student() {id = 0;name = "匿名";score = 0.0f;}
private:int id;std::string name;float score;
};Student stu; // 调用默认构造函数,id=0,name="匿名",score=0.0f
(2)带参构造函数
定义与作用
- 包含参数:通过参数为成员变量赋初始值,支持灵活初始化。
- 参数作用域:参数名可与成员变量同名,通过
this
指针区分(this->成员变量
)。
示例:通过参数初始化成员
class Circle {
public:// 带参构造函数:初始化半径和面积Circle(float r) {radius = r; // 成员变量radius = 参数rarea = 3.14f * r * r; // 计算初始面积}
private:float radius;float area;
};Circle c(5.0f); // 创建半径5.0的圆,area自动计算为78.5
(3)初始化列表(Constructor Initializer List)
语法与格式
- 位置:在构造函数参数列表后,使用
:
分隔,多个成员用逗号分隔。 - 格式:
构造函数(参数列表) : 成员1(值1), 成员2(值2), ... { 函数体 }
核心优势
- 效率更高:直接调用成员的构造函数(如类成员对象),避免先默认构造再赋值。
- 必需场景:初始化
const
成员或引用成员(二者必须在定义时初始化)。
示例:初始化列表的使用
class Person {
public:// 普通成员与const成员的初始化Person(std::string n, int a) : name(n), age(a) { // 函数体可空,初始化在列表完成}// 引用成员必须通过初始化列表赋值Person(std::string n, int a, int& ref) : name(n), age(a), ref_num(ref) { }
private:std::string name;int age;const int ref_num; // const引用成员,必须在初始化列表赋值
};// 初始化const成员的错误与正确写法对比
class ErrorDemo {const int value;
public:ErrorDemo() { value = 10; } // 错误!const成员不能赋值
};class CorrectDemo {const int value;
public:CorrectDemo() : value(10) { } // 正确!通过初始化列表赋值
};
(4)构造函数重载
重载规则
- 函数名相同,但参数列表不同(参数个数、类型、顺序至少有一个不同)。
- 返回值类型无关:不能通过返回值区分重载构造函数。
示例:多种初始化方式
class Vector {
public:// 无参构造:初始化零向量Vector() : x(0), y(0) {}// 单参数构造:二维向量(x=y)Vector(float val) : x(val), y(val) {}// 双参数构造:指定x和yVector(float x_val, float y_val) : x(x_val), y(y_val) {}
private:float x, y;
};// 调用不同构造函数
Vector v1; // 无参构造,(0, 0)
Vector v2(5.0f); // 单参构造,(5.0, 5.0)
Vector v3(3.0f, 4.0f); // 双参构造,(3.0, 4.0)
C++14 简化写法:= default
- 作用:显式让编译器生成默认构造函数,保持代码简洁。
- 示例:
class Simple { public:Simple() = default; // 等价于空的默认构造函数Simple(int x) : data(x) {} private:int data; };
3. 初始化 const 成员的强制要求
为什么必须用初始化列表?
const
成员在声明后不能被赋值(只能初始化),而构造函数的函数体执行时,成员变量已完成定义,无法再对const
成员赋值。- 初始化列表在成员变量定义时直接赋值,满足
const
的初始化要求。
示例:const 成员的正确初始化
class MathConstants {
public:// 初始化const成员PI和引用成员epsilon(引用必须初始化)MathConstants(float eps) : PI(3.14159f), epsilon(eps) {}
private:const float PI; // 圆周率,固定值float& epsilon; // 精度引用,必须在初始化列表赋值
};// 错误示例:试图在函数体中赋值const成员
class ErrorCase {const int value;
public:ErrorCase(int v) { value = v; } // 编译错误!const成员不能赋值
};// 正确示例:通过初始化列表赋值
class CorrectCase {const int value;
public:CorrectCase(int v) : value(v) {} // 正确
};
4. 构造函数的最佳实践
(1)统一使用初始化列表
- 无论是否为
const
成员,优先在初始化列表中赋值,提升效率(尤其对类成员对象)。
(2)避免冗余初始化
- 若成员变量无需特殊处理,可依赖编译器默认初始化(如
std::string
默认构造为空字符串)。
(3)处理动态资源
- 在构造函数中分配资源(如
new
内存),并在析构函数中释放(确保资源配对)。class DynamicArray { public:DynamicArray(int size) : data(new int[size]), length(size) {}~DynamicArray() { delete[] data; } // 析构函数释放内存 private:int* data;int length; };
总结:构造函数核心知识点
特性 | 说明 |
---|---|
必需性 | 创建对象时必须调用构造函数,编译器自动生成默认构造函数(无其他构造函数时)。 |
初始化方式 | 普通成员可在函数体赋值,const 成员和引用成员必须通过初始化列表初始化。 |
重载规则 | 参数列表不同(个数、类型、顺序),支持灵活的对象创建方式。 |
资源管理 | 构造函数分配资源,析构函数释放资源,确保内存安全。 |
通过合理设计构造函数,开发者能确保对象在创建时处于有效状态,为后续操作奠定基础。下一节将深入解析析构函数与对象生命周期管理,进一步理解 C++ 对象的完整生命周期。
四、析构函数:对象的清理
在 C++ 中,对象的生命周期管理至关重要。构造函数负责对象的初始化,而析构函数则承担着对象销毁时的清理工作,确保资源正确释放,避免内存泄漏。下面从基础概念到实战应用逐步解析析构函数的核心知识。
1. 析构函数概述
语法规则
- 函数名:以
~
符号开头,后跟类名(与构造函数对应),例如~ClassName()
。 - 参数与返回值:没有参数,也没有返回值(不能写
void
)。 - 调用时机:
- 栈对象离开作用域时(如函数结束)。
- 堆对象通过
delete
释放时(如delete p;
)。 - 程序结束时(全局对象和静态对象销毁)。
核心作用
- 资源释放:释放构造函数或成员函数分配的资源(如动态内存
new
、文件句柄fopen
、网络连接等)。 - 数据清理:重置成员变量状态,避免无效引用。
与构造函数的关系
- 配对使用:构造函数分配资源,析构函数释放资源,形成 “资源管理对”(RAII 模式的基础)。
- 执行顺序:构造函数按 “基类→派生类” 顺序执行,析构函数按 “派生类→基类” 逆序执行(确保派生类资源先释放,基类后释放)。
2. 示例:释放动态内存(核心场景)
需求:管理动态数组的生命周期
当类中包含动态分配的内存(如new
创建的数组),必须在析构函数中用delete
释放,否则会导致内存泄漏。
代码实现
#include <iostream>
using namespace std;class DynamicArray {
private:int* data; // 动态数组指针int size; // 数组大小public:// 构造函数:分配内存并初始化DynamicArray(int s) : size(s) {data = new int[size]; // 分配size个int的内存空间for (int i = 0; i < size; i++) {data[i] = i; // 初始化数组元素}cout << "构造函数:分配内存,地址=" << data << endl;}// 析构函数:释放动态内存~DynamicArray() {delete[] data; // 释放数组内存(与new[]配对)data = nullptr; // 置空指针,避免野指针cout << "析构函数:释放内存,地址=" << data << endl;}
};int main() {// 栈对象:离开main作用域时自动调用析构函数{DynamicArray arr(5); // 构造函数执行,分配内存} // 作用域结束,析构函数自动调用// 堆对象:显式调用delete时触发析构函数DynamicArray* ptr = new DynamicArray(3); // 构造函数执行delete ptr; // 手动释放,析构函数调用return 0;
}
代码解释
-
构造函数:
- 接收参数
size
,使用new[]
分配动态数组内存。 - 初始化数组元素,输出内存地址以便观察。
- 接收参数
-
析构函数:
- 使用
delete[]
释放动态数组(必须与new[]
配对,单个对象用delete
)。 - 释放后将指针置为
nullptr
,防止后续误操作(野指针问题)。
- 使用
-
调用场景:
- 栈对象
arr
在离开花括号作用域时,自动调用析构函数。 - 堆对象
ptr
通过delete
显式释放,触发析构函数。
- 栈对象
3. 注意事项与进阶知识
(1)默认析构函数:编译器的 “隐形助手”
- 自动生成条件:若用户未定义析构函数,编译器会生成一个默认析构函数(空函数)。
class Simple {int value; // 无自定义析构函数,编译器生成~Simple() {} };
- 局限性:默认析构函数仅能释放非动态资源(如基本类型、标准库对象),对
new
分配的内存、文件句柄等无效,需手动定义析构函数。
(2)析构函数与继承:顺序至关重要
-
基类与派生类的执行顺序:
- 创建派生类对象时:先执行基类构造函数 → 再执行派生类构造函数。
- 销毁派生类对象时:先执行派生类析构函数 → 再执行基类析构函数(与构造顺序相反)。
class Base { public:~Base() { cout << "Base析构" << endl; } }; class Derived : public Base { public:~Derived() { cout << "Derived析构" << endl; } };int main() {Derived obj; // 输出:Base构造 → Derived构造(假设存在构造函数)// 销毁时输出:Derived析构 → Base析构return 0; }
(3)析构函数不能重载
- 原因:析构函数没有参数列表,无法通过参数区分不同版本,因此每个类最多有一个析构函数。
(4)析构函数与异常处理
- 原则:析构函数中避免抛出异常,否则可能导致程序终止。
- 处理方式:若必须处理异常,应在析构函数内部捕获并处理,而非抛出。
~DynamicArray() {try {delete[] data;} catch (...) {// 处理异常或记录日志} }
4. 最佳实践:资源管理的黄金法则
(1)RAII 模式(资源获取即初始化)
- 核心思想:通过构造函数获取资源,析构函数释放资源,确保资源生命周期与对象绑定。
- 典型应用:
- 动态内存:
new
/delete
配对。 - 文件操作:构造函数打开文件,析构函数关闭文件。
class FileHandler { public:FileHandler(const char* path) {file = fopen(path, "r"); // 构造函数打开文件}~FileHandler() {if (file) fclose(file); // 析构函数关闭文件} private:FILE* file; };
- 动态内存:
(2)避免手动管理资源:使用智能指针
- C++11 引入
std::unique_ptr
和std::shared_ptr
,自动管理动态内存,无需手动编写析构函数。#include <memory> class ModernArray { private:std::unique_ptr<int[]> data; // 智能指针自动释放内存 public:ModernArray(int size) : data(new int[size]) {} // 无需析构函数 };
总结:析构函数核心知识点
特性 | 说明 |
---|---|
语法特征 | 以~类名 命名,无参数、无返回值,自动调用于对象销毁时。 |
核心作用 | 释放动态资源(如new 内存、文件句柄),防止内存泄漏。 |
默认行为 | 未定义时编译器生成空析构函数,仅适用于无动态资源的类。 |
继承场景 | 析构顺序与构造顺序相反(派生类→基类),确保资源正确释放。 |
最佳实践 | 结合 RAII 模式,或使用智能指针简化资源管理,避免手动编写繁琐析构逻辑。 |
通过合理设计析构函数,开发者能有效管理对象生命周期,确保程序的稳定性和资源利用率。下一节将深入探讨this
指针与静态成员,进一步理解类的内部机制。
五、this 指针:指向当前对象的 “隐形指针”
1. this 指针作用
- 解决命名冲突:当成员变量与参数同名时,用
this->成员名
区分。class Person { private:char name[20];int age; public:void set_name(char* name) {strcpy(this->name, name); // this->name是成员变量,name是参数} };
2. 隐含参数
- 成员函数隐式包含
this
指针参数,调用时自动传递当前对象地址。Person p; p.set_name("Alice"); // 等价于 set_name(&p, "Alice")
3. 返回当前对象引用(链式调用)
class Counter {
private:int value;
public:Counter& add(int n) {value += n;return *this; // 返回当前对象引用}
};Counter c;
c.add(10).add(20); // 链式调用,等价于 c.add(10); c.add(20);
六、静态成员:类级别的共享数据与函数
1. 静态成员变量
-
定义:用
static
修饰,属于类而非对象,所有对象共享。 -
初始化:必须在类外初始化,格式为
类型 类名::变量名 = 初始值;
。class Student { public:static int total_students; // 声明静态成员变量 }; int Student::total_students = 0; // 类外初始化
-
访问方式:
- 通过类名:
Student::total_students
- 通过对象:
stu1.total_students
(需公有权限)
- 通过类名:
2. 静态成员函数
- 特点:只能访问静态成员(变量 / 函数),无
this
指针。 - 应用场景:统计类的对象数量。
class Student { public:static int get_total() { // 静态成员函数return total_students;} };
七、const 成员:保护数据不被修改
1. const 成员变量
- 初始化:必须通过构造函数初始化列表赋值,不可修改。
class Math { private:const float PI; // const成员变量 public:Math() : PI(3.14f) {} // 初始化列表赋值 };
2. const 成员函数(常成员函数)
- 语法:在函数声明后加
const
,保证不修改成员变量。class Circle { private:float radius; public:float get_radius() const { // 常成员函数return radius; // 不可修改radius} };
3. const 对象
- 定义:
const 类名 对象名;
,只能调用常成员函数。const Circle c(5.0); c.get_radius(); // 合法,调用常成员函数 // c.set_radius(6.0); // 非法,set_radius非const函数
八、友元:打破访问权限的 “特权”
在 C++ 中,类的封装性通过public/protected/private
严格控制成员访问,但有时需要允许特定的函数或类突破这种限制,直接访问私有成员。** 友元(Friend)** 机制就是为此设计的 “特权通道”,它允许非类成员函数或其他类的成员函数访问当前类的私有 / 保护成员。
1. 友元函数
友元函数是获得类访问特权的非成员函数,分为两种类型:全局非成员友元函数和其他类的成员友元函数。
(1)非成员友元函数
定义与声明
- 作用:允许一个全局函数(不属于任何类)访问类的私有 / 保护成员。
- 声明方式:在类体内用
friend
关键字声明函数原型,格式为:friend 返回值类型 函数名(参数列表);
- 关键特性:友元函数不是类的成员,无需通过对象调用,但可访问类的所有成员。
示例:访问银行账户余额(私有成员)
#include <iostream>
using namespace std; class BankAccount {
private: float balance; // 私有成员:账户余额 public: // 声明全局函数display_balance为友元 friend void display_balance(const BankAccount& acc); // 构造函数初始化余额 BankAccount(float bal) : balance(bal) {}
}; // 友元函数定义:可直接访问私有成员balance
void display_balance(const BankAccount& acc) { cout << "账户余额:" << acc.balance << " 元" << endl;
} int main() { BankAccount account(10000.5f); display_balance(account); // 合法!友元函数访问私有成员 return 0;
}
代码解析
- 友元声明:
friend void display_balance(...)
在类内声明,赋予该函数访问私有成员balance
的权限。 - 参数传递:使用
const引用
传递对象,避免拷贝构造,提高效率并防止修改原始对象。
(2)其他类的成员友元函数
应用场景
当类 B 的某个成员函数需要访问类 A 的私有成员时,可将该成员函数声明为类 A 的友元。
声明方式
class A {
private: int private_data;
public: // 声明类B的成员函数B::friend_func为友元 friend void B::friend_func(A& obj);
}; class B {
public: void friend_func(A& obj) { obj.private_data = 100; // 合法!访问A的私有成员 }
};
示例:类间协作访问私有成员
#include <iostream>
using namespace std; class Teacher; // 前向声明,解决类依赖 class Student {
private: int student_id;
public: // 声明Teacher类的成员函数Teacher::view_id为友元 friend void Teacher::view_id(Student& stu); Student(int id) : student_id(id) {}
}; class Teacher {
public: void view_id(Student& stu) { cout << "学生ID:" << stu.student_id << endl; // 访问私有成员 }
}; int main() { Student stu(20230001); Teacher teacher; teacher.view_id(stu); // 教师类成员函数访问学生类私有ID return 0;
}
注意事项
- 前向声明:若友元函数所属的类未定义,需提前声明(如
class Teacher;
),但不能访问其成员细节。 - 单向特权:仅被声明的成员函数拥有访问权,类 B 的其他成员函数仍无权限。
2. 友元类
定义与声明
- 作用:将整个类 B 声明为类 A 的友元,类 B 的所有成员函数均可访问类 A 的私有 / 保护成员。
- 声明方式:在类 A 内用
friend class B;
声明类 B 为友元。
示例:友元类访问私有成员
#include <iostream>
using namespace std; class Library; // 前向声明 class Book {
private: string title; int page_count; public: Book(string t, int p) : title(t), page_count(p) {} // 声明Library为友元类 friend class Library;
}; class Library {
public: void display_book_info(Book& b) { // 访问Book的私有成员 cout << "书名:" << b.title << ", 页数:" << b.page_count << endl; }
}; int main() { Book book("C++ Primer", 1000); Library lib; lib.display_book_info(book); // 合法!友元类成员函数访问私有成员 return 0;
}
友元类的特性
-
单向性:
- 若 A 是 B 的友元类,B 不一定是 A 的友元类,除非 A 也声明 B 为友元。
class A { friend class B; }; // A允许B访问自己的私有成员 class B { friend class A; }; // 需额外声明,B才允许A访问自己的私有成员
-
不可传递性:
- 若 B 是 A 的友元,C 是 B 的友元,C 并非自动成为 A 的友元。
-
破坏封装性:
- 友元类可访问所有私有成员,打破类的封装边界,需谨慎使用(仅在必要的类间协作时使用)。
3. 友元的优缺点与最佳实践
(1)核心优势
- 灵活协作:允许类间高效交互,避免通过公有接口间接访问(如性能敏感场景)。
- 保留封装性:仅对特定函数 / 类开放权限,而非完全公开私有成员。
(2)潜在风险
- 耦合度增加:友元关系会强化类间依赖,修改一方可能影响另一方。
- 调试难度:私有成员的访问点分散在友元函数 / 类中,难以追踪。
(3)使用建议
- 最小特权原则:优先声明单个友元函数而非整个友元类,缩小特权范围。
- 文档说明:在友元声明处注释说明原因,提高代码可读性。
- 避免滥用:仅在必要时使用(如运算符重载、类间数据共享),优先通过公有接口实现。
总结:友元核心知识点
类型 | 定义 | 声明方式 | 访问权限 |
---|---|---|---|
非成员友元函数 | 全局函数,通过friend 声明获得类私有成员访问权 | friend void func(类对象); | 可访问类的所有成员(含私有) |
成员友元函数 | 其他类的成员函数,声明后可访问当前类私有成员 | friend void 类B::func(类A对象); | 仅该成员函数拥有访问权 |
友元类 | 整个类的所有成员函数可访问当前类私有成员 | friend class 类B; | 类 B 的所有成员函数均有访问权 |
友元机制是 C++ 封装性的补充,合理使用能在保持代码结构的同时实现高效交互。但需注意控制特权范围,避免过度依赖,确保代码的可维护性和安全性。下一章节将深入探讨拷贝构造函数与对象复制,理解对象创建的深层机制。
九、拷贝构造函数:对象的 “复制粘贴”
1. 自定义拷贝构造函数
- 语法:参数为当前类的
const引用
,避免递归调用。class String { private:char* str; public:String(char* s) { // 普通构造函数str = new char[strlen(s)+1];strcpy(str, s);}String(const String& obj) { // 拷贝构造函数str = new char[strlen(obj.str)+1];strcpy(str, obj.str); // 深拷贝,避免浅拷贝问题}~String() { delete[] str; } };
2. 默认拷贝构造函数
- 自动生成:若未定义,编译器生成默认版本(浅拷贝),适用于无动态资源的类。
- 风险:若类包含动态内存,默认拷贝会导致多个对象指向同一块内存,释放时崩溃。
3. 调用场景
- 对象初始化:
String s2 = s1;
或String s2(s1);
- 函数传参:
void func(String obj);
调用时复制实参对象。 - 函数返回:
String func() { String s; return s; }
返回时复制对象。
十、总结:类与对象核心知识点
概念 | 关键特性 |
---|---|
类的定义 | 封装数据与行为,通过public/protected/private 控制访问权限。 |
对象初始化 | 构造函数(含参数、初始化列表、重载),自动调用,初始化成员数据。 |
资源管理 | 析构函数释放资源,避免内存泄漏,与构造函数成对出现。 |
数据共享 | 静态成员(变量 / 函数)属于类,所有对象共享,类外初始化。 |
权限突破 | 友元函数 / 类可访问私有成员,打破封装限制,需谨慎使用。 |
对象复制 | 拷贝构造函数实现深拷贝,避免默认浅拷贝的内存问题。 |
类型安全 | const 成员保证数据不被修改,常对象仅能调用常成员函数。 |
通过以上知识点,我们掌握了 C++ 类与对象的核心机制,从封装数据到管理对象生命周期,再到灵活处理对象间的关系。后续将深入学习继承与多态,进一步体会面向对象编程的强大能力。