从C到C++:深入理解类与对象及运算符重载
C语言作为面向过程的编程语言,以函数为核心组织代码,数据与操作数据的函数往往是分离的。这种模式在处理简单问题时高效直接,但面对复杂系统时,会暴露出代码复用性低、数据安全性差等问题。C++在C的基础上引入了面向对象编程(OOP)思想,其核心是类(Class) 与对象(Object),并通过函数重载、运算符重载等特性增强了代码的灵活性。本文将从C语言的局限出发,逐步解析C++中类与对象的关系,以及函数和运算符重载的本质,并通过丰富的代码示例加深理解。
一、从C的结构体到C++的类:封装的诞生
C语言中,结构体(struct)可以用来聚合不同类型的数据,但无法包含操作这些数据的函数。例如,要表示一个"学生"并实现其相关操作,C语言通常这样写:
// C语言示例:学生数据与操作分离
#include <stdio.h>
#include <string.h>
// 仅包含数据的结构体
struct Student {
char name[20];
int age;
};
// 操作结构体的函数(与数据分离)
void set_name(struct Student* s, const char* name) {
strcpy(s->name, name);
}
void set_age(struct Student* s, int age) {
s->age = age; // 直接修改数据,无保护机制
}
void print_student(struct Student* s) {
printf("Name: %s, Age: %d\n", s->name, s->age);
}
int main() {
struct Student s;
set_name(&s, "Tom");
set_age(&s, 18);
print_student(&s); // 输出:Name: Tom, Age: 18
// 隐患:可以直接修改内部数据,无需校验
s.age = -5;
print_student(&s); // 输出:Name: Tom, Age: -5(不合理但允许)
return 0;
}
这种写法的问题在于:数据(name、age)完全暴露,任何函数都能直接修改,容易导致误操作;数据与函数分离,代码组织混乱,当结构体和函数增多时难以维护。
C++的类解决了这个问题:类将数据(成员变量)和操作数据的函数(成员函数)封装在一起,并通过访问控制符(public、private、protected)控制数据的访问权限,实现"数据隐藏"。
// C++示例:用类封装学生数据与操作
#include <iostream>
#include <string>
class Student {
private: // 私有成员:仅类内部可访问(外部不可直接修改)
std::string name;
int age;
public: // 公有成员:类外部可访问(提供安全的操作接口)
// 成员函数:操作私有数据
void set_name(const std::string& n) {
name = n; // 内部安全修改
}
void set_age(int a) {
if (a > 0 && a < 150) { // 加入数据校验,避免不合理值
age = a;
} else {
std::cout << "Invalid age! Set to default 0." << std::endl;
age = 0;
}
}
void print() const { // const成员函数:承诺不修改成员变量
std::cout << "Name: " << name << ", Age: " << age << std::endl;
}
// 获取私有成员的接口(只读,不允许修改)
int get_age() const {
return age;
}
};
int main() {
Student s; // 创建对象(类的实例)
s.set_name("Tom"); // 通过公有函数操作数据
s.set_age(18);
s.print(); // 输出:Name: Tom, Age: 18
s.set_age(-5); // 触发校验:Invalid age! Set to default 0.
s.print(); // 输出:Name: Tom, Age: 0
// s.age = 20; // 编译错误:私有成员不可直接访问
std::cout << "Current age: " << s.get_age() << std::endl; // 输出:0(通过接口读取)
return 0;
}
核心概念:类与对象的关系
| 概念 | 定义 | 类比示例 |
|---|---|---|
| 类(Class) | 自定义数据类型,作为"模板"定义对象的属性(成员变量)和行为(成员函数) | 蓝图 |
| 对象(Object) | 类的实例,根据类模板创建的具体实体 | 根据图纸生产的具体 |
扩展示例:多个对象的创建与使用
一个类可以创建多个对象,每个对象拥有独立的成员变量(共享成员函数代码):
int main() {
Student s1, s2; // 创建两个Student对象
s1.set_name("Alice");
s1.set_age(20);
s2.set_name("Bob");
s2.set_age(22);
s1.print(); // Name: Alice, Age: 20
s2.print(); // Name: Bob, Age: 22
// 对象数组
Student class3[3];
class3[0].set_name("Charlie");
class3[0].set_age(19);
class3[1].set_name("Diana");
class3[1].set_age(21);
class3[2].set_name("Eve");
class3[2].set_age(20);
for (int i = 0; i < 3; i++) {
class3[i].print();
}
return 0;
}
C结构体与C++类的核心差异
| 特性 | C结构体 | C++类 |
|---|---|---|
| 成员包含 | 仅数据(变量) | 数据(成员变量)+ 函数(成员函数) |
| 访问控制 | 所有成员默认公开(无访问控制) | 支持public/private/protected |
| 数据安全性 | 低(可直接修改内部数据) | 高(通过接口访问,可加校验) |
| 代码组织 | 数据与操作分离 | 数据与操作封装为整体 |
二、类的特殊成员函数:构造与析构
C语言中,结构体的初始化需要手动调用函数(如set_name),容易遗漏。C++为类提供了构造函数和析构函数,自动完成对象的初始化和清理。
1. 构造函数
| 特性 | 说明 |
|---|---|
| 作用 | 创建对象时自动调用,初始化成员变量 |
| 语法 | 与类同名,无返回值(无需写void) |
| 重载性 | 可重载(通过参数列表区分) |
| 默认构造函数 | 若未自定义,编译器自动生成(无参,不做任何操作) |
| 初始化方式 | 可通过函数体赋值或成员初始化列表(更高效,直接初始化而非赋值) |
class Student {
private:
std::string name;
int age;
public:
// 无参构造函数(默认构造函数)
Student() {
name = "Unknown";
age = 0;
std::cout << "Default constructor called" << std::endl;
}
// 带参构造函数(重载)
Student(const std::string& n, int a) {
name = n;
set_age(a); // 复用已有函数
std::cout << "Parameter constructor called" << std::endl;
}
// 成员初始化列表:更高效的初始化方式(直接初始化,而非先默认构造再赋值)
Student(const std::string& n) : name(n), age(0) {
std::cout << "Single parameter constructor called" << std::endl;
}
// ... 其他成员函数同上
};
int main() {
Student s1; // 调用无参构造函数
s1.print(); // Name: Unknown, Age: 0
Student s2("Jerry", 20); // 调用带参构造函数
s2.print(); // Name: Jerry, Age: 20
Student s3("Frank"); // 调用单参数构造函数
s3.print(); // Name: Frank, Age: 0
return 0;
}
2. 析构函数
| 特性 | 说明 |
|---|---|
| 作用 | 对象销毁时自动调用,释放资源(如动态内存、文件句柄、网络连接等) |
| 语法 | 类名前加~,无参数,无返回值 |
| 重载性 | 不可重载(每个类只有一个析构函数) |
| 调用时机 | 对象离开作用域、被delete销毁或程序结束时 |
class FileHandler {
private:
FILE* file; // 文件指针(需要手动关闭)
public:
// 构造函数:打开文件
FileHandler(const char* filename, const char* mode) {
file = fopen(filename, mode);
if (file == nullptr) {
std::cout << "Failed to open file!" << std::endl;
} else {
std::cout << "File opened successfully" << std::endl;
}
}
// 析构函数:关闭文件(确保资源释放)
~FileHandler() {
if (file != nullptr) {
fclose(file);
std::cout << "File closed successfully" << std::endl;
}
}
// 写入数据的接口
void write(const char* content) {
if (file != nullptr) {
fputs(content, file);
}
}
};
int main() {
{ // 作用域:限制对象生命周期
FileHandler fh("test.txt", "w");
fh.write("Hello, FileHandler!");
// 离开作用域时,fh自动销毁,调用析构函数关闭文件
}
// 输出:File opened successfully → File closed successfully
return 0;
}
3. 拷贝构造函数(补充关键特性)
当用一个对象初始化另一个对象时(如Student s2 = s1;),编译器会自动生成拷贝构造函数。但默认拷贝构造是"浅拷贝"(仅复制成员变量的值),当类包含动态内存时会出问题:
| 拷贝类型 | 特点 | 适用场景 |
|---|---|---|
| 浅拷贝 | 仅复制成员变量的值(指针变量复制地址,不复制指向的内容) | 类中无动态内存(如基本类型) |
| 深拷贝 | 为指针变量重新分配内存,并复制指向的内容 | 类中包含动态内存(如new分配的数组) |
class Array {
private:
int* data; // 动态数组
int size;
public:
// 构造函数:分配内存
Array(int s) : size(s) {
data = new int[size];
std::cout << "Array constructed, size: " << size << std::endl;
}
// 析构函数:释放内存
~Array() {
delete[] data;
std::cout << "Array destroyed" << std::endl;
}
// 手动实现拷贝构造函数(深拷贝)
Array(const Array& other) : size(other.size) {
data = new int[size]; // 重新分配内存
for (int i = 0; i < size; i++) {
data[i] = other.data[i]; // 复制数据
}
std::cout << "Copy constructor called" << std::endl;
}
};
int main() {
Array arr1(5);
Array arr2 = arr1; // 用arr1初始化arr2(调用拷贝构造)
// 若未实现深拷贝的拷贝构造,会导致arr1.data和arr2.data指向同一块内存
// 析构时会重复释放,导致程序崩溃
return 0;
}
三、函数重载:同名函数的多态性
C语言中,函数名必须唯一,若要实现"打印int"和"打印字符串",需定义print_int、print_str等不同名函数。C++允许函数重载:同一作用域内,函数名相同但参数列表(类型、个数、顺序)不同的函数。
函数重载的规则
| 规则 | 示例是否构成重载 | 说明 |
|---|---|---|
| 函数名相同,参数类型不同 | 是 | print(int)与print(double) |
| 函数名相同,参数个数不同 | 是 | print(int)与print(int, int) |
| 函数名相同,参数顺序不同 | 是 | print(int, double)与print(double, int) |
| 仅返回值类型不同 | 否 | int print(int)与double print(int)(调用时无法区分,编译错误) |
#include <iostream>
#include <string>
// 1. 参数类型不同的重载
void print(int x) {
std::cout << "int: " << x << std::endl;
}
void print(double x) {
std::cout << "double: " << x << std::endl;
}
void print(const std::string& s) {
std::cout << "string: " << s << std::endl;
}
// 2. 参数个数不同的重载
void print(int a, int b) {
std::cout << "int + int: " << a << ", " << b << std::endl;
}
// 3. 参数顺序不同的重载
void print(int a, double b) {
std::cout << "int then double: " << a << ", " << b << std::endl;
}
void print(double a, int b) {
std::cout << "double then int: " << a << ", " << b << std::endl;
}
int main() {
print(10); // 调用print(int) → int: 10
print(3.14); // 调用print(double) → double: 3.14
print("Hello"); // 调用print(string) → string: Hello
print(5, 6); // 调用print(int, int) → int + int: 5, 6
print(5, 3.14); // 调用print(int, double) → int then double: 5, 3.14
print(3.14, 5); // 调用print(double, int) → double then int: 3.14, 5
return 0;
}
函数重载的本质:编译器会根据参数列表对函数名进行"重命名"(如print(int)可能被改为_Z5printi),因此底层仍满足"函数名唯一",这一过程称为名字修饰(Name Mangling)。
四、运算符重载:让自定义类型支持运算符
C语言中,运算符(如+、=、<<)只能用于内置类型(int、double等)。C++允许运算符重载:重新定义运算符对自定义类型(类/结构体)的操作,让代码更直观。
运算符重载的实现方式
| 实现方式 | 特点 | 示例(a + b) |
|---|---|---|
| 成员函数 | 左操作数为当前对象(this指针),参数为右操作数 | a.operator+(b) |
| 全局函数 | 需显式传入两个操作数,通常声明为类的友元以访问私有成员 | operator+(a, b) |
语法:返回值类型 operator运算符(参数列表)
示例1:复数类的加减与输出
复数由实部和虚部组成,我们希望实现3+4i与1+2i的加减(结果为4+6i/2+2i),以及用cout直接输出复数。
#include <iostream>
class Complex {
private:
double real; // 实部
double imag; // 虚部
public:
// 构造函数(带默认参数)
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// 成员函数重载 + :当前对象 + 另一个复数
Complex operator+(const Complex& other) const {
return Complex(real + other.real, imag + other.imag);
}
// 成员函数重载 - :当前对象 - 另一个复数
Complex operator-(const Complex& other) const {
return Complex(real - other.real, imag - other.imag);
}
// 友元函数:允许全局函数访问私有成员
friend std::ostream& operator<<(std::ostream& os, const Complex& c);
};
// 全局函数重载 << :cout << 复数(支持链式输出)
std::ostream& operator<<(std::ostream& os, const Complex& c) {
os << c.real << (c.imag >= 0 ? " + " : " - ") << std::abs(c.imag) << "i";
return os; // 支持 cout << c1 << c2
}
int main() {
Complex c1(3, 4), c2(1, 2), c3(5, -3);
Complex sum = c1 + c2; // 等价于 c1.operator+(c2)
Complex diff = c1 - c2; // 等价于 c1.operator-(c2)
std::cout << "c1 = " << c1 << std::endl; // 3 + 4i
std::cout << "c2 = " << c2 << std::endl; // 1 + 2i
std::cout << "c3 = " << c3 << std::endl; // 5 - 3i(注意符号处理)
std::cout << "c1 + c2 = " << sum << std::endl; // 4 + 6i
std::cout << "c1 - c2 = " << diff << std::endl; // 2 + 2i
return 0;
}
示例2:自增运算符++的重载(前缀与后缀)
自增运算符有前缀(++a)和后缀(a++)两种形式,重载时需通过参数区分:
class Counter {
private:
int count;
public:
Counter(int c = 0) : count(c) {}
// 前缀++:先自增,再返回当前对象
Counter& operator++() {
count++;
return *this; // 返回引用,支持连续++:++(++a)
}
// 后缀++:参数加int(无实际意义,仅用于区分),先返回原对象,再自增
Counter operator++(int) {
Counter temp = *this; // 保存当前状态
count++;
return temp; // 返回临时对象(原状态)
}
friend std::ostream& operator<<(std::ostream& os, const Counter& c);
};
std::ostream& operator<<(std::ostream& os, const Counter& c) {
os << c.count;
return os;
}
int main() {
Counter a(5);
std::cout << "初始 a = " << a << std::endl; // 5
Counter b = ++a; // 前缀++:a先增为6,b=6
std::cout << "a = " << a << ", b = " << b << std::endl; // 6, 6
Counter c = a++; // 后缀++:先将a=6赋值给c,a再增为7
std::cout << "a = " << a << ", c = " << c << std::endl; // 7, 6
return 0;
}
运算符重载的限制
| 类别 | 说明 |
|---|---|
| 不可重载的运算符 | .(成员访问)、.*(成员指针访问)、::(作用域)、? :(三目运算符)、sizeof |
| 限制 | 不能创造新运算符;重载后优先级和结合性不变;操作数个数不能改变(如+仍需两个操作数) |
五、总结:从面向过程到面向对象的跨越
C语言的核心是"过程":通过函数一步步处理数据,数据与操作分离。C++引入的类与对象将数据和操作封装为一个整体,通过访问控制保证数据安全,通过构造/析构函数自动管理资源,解决了C语言的结构性缺陷。
| 特性 | 核心价值 |
|---|---|
| 类与对象 | 实现数据与操作的封装,将现实世界的实体抽象为代码中的"类",提升代码的模块化 |
| 函数重载 | 允许同名函数处理不同参数,减少函数名冗余,提升代码可读性 |
| 运算符重载 | 让自定义类型支持自然的运算符操作,使代码更直观(如c1 + c2代替c1.add(c2)) |
0

被折叠的 条评论
为什么被折叠?



