1. C++面向对象编程基础:封装与类设计
1.1 封装的概念与实现
封装是C++面向对象编程的三大特性之一(另外两个是继承和多态)。简单来说,封装就是把数据(属性)和操作数据的方法(行为)捆绑在一起,形成一个"黑盒子"。就像我们日常使用的手机,我们不需要知道内部电路如何工作,只需要通过外部的按钮和屏幕就能使用所有功能。
1.1.1 封装的基本语法
在C++中,我们使用class关键字来定义一个类。一个基本的类定义包含以下部分:
cpp复制class 类名 {
访问权限:
// 属性(成员变量)
// 行为(成员函数)
};
让我们通过一个具体的例子来理解封装。假设我们要设计一个圆类,用来计算圆的周长:
cpp复制#include<iostream>
using namespace std;
const double PI = 3.14;
class Circle {
public: // 公共访问权限
// 属性
int m_r; // 半径
// 行为
double calculateZC() { // 计算周长
return 2 * PI * m_r;
}
};
int main() {
Circle c1; // 创建圆对象
c1.m_r = 10; // 给半径赋值
cout << "周长为:" << c1.calculateZC() << endl; // 输出:62.8
return 0;
}
在这个例子中,Circle类封装了圆的属性(半径m_r)和行为(计算周长的方法calculateZC())。通过将相关数据和操作捆绑在一起,代码更加清晰和易于维护。
1.1.2 学生类设计实例
让我们再看一个学生类的例子,进一步理解封装:
cpp复制#include<iostream>
#include<string>
using namespace std;
class Student {
public:
// 属性
string m_Name; // 姓名
int m_Id; // 学号
// 行为
void showStudent() {
cout << "姓名:" << m_Name << " 学号:" << m_Id << endl;
}
void setName(string name) {
m_Name = name;
}
void setId(int id) {
m_Id = id;
}
};
int main() {
Student s1;
s1.setName("张三");
s1.setId(1);
s1.showStudent(); // 输出:姓名:张三 学号:1
Student s2;
s2.m_Name = "李四"; // 也可以直接访问public成员
s2.m_Id = 2;
s2.showStudent(); // 输出:姓名:李四 学号:2
return 0;
}
注意事项:
- 类中的属性和行为统称为"成员"
- 属性也称为"成员属性"或"成员变量"
- 行为也称为"成员函数"或"成员方法"
- 在实际开发中,建议使用set/get方法而不是直接访问成员变量,这样可以更好地控制数据的访问和修改
1.2 访问权限控制
C++提供了三种访问权限,用于控制类成员的访问范围:
- public(公共权限):成员在类内和类外都可以访问
- protected(保护权限):成员在类内可以访问,类外不能访问,子类可以访问父类的保护成员
- private(私有权限):成员只能在类内访问,类外和子类都不能访问(默认权限)
cpp复制#include<iostream>
#include<string>
using namespace std;
class Person {
public:
string m_Name; // 公共权限
protected:
string m_Car; // 保护权限
private:
int m_Password; // 私有权限(银行卡密码)
public:
void func() {
m_Name = "张三";
m_Car = "拖拉机";
m_Password = 123456;
cout << "姓名:" << m_Name << " 车:" << m_Car
<< " 密码:" << m_Password << endl;
}
};
int main() {
Person p;
p.m_Name = "李四"; // 可以访问
// p.m_Car = "电动车"; // 错误:保护权限,类外不能访问
// p.m_Password = 66666; // 错误:私有权限,类外不能访问
p.func(); // 通过公共方法访问私有和保护成员
return 0;
}
1.2.1 struct和class的区别
在C++中,struct和class的唯一区别就是默认的访问权限不同:
- struct默认权限为public
- class默认权限为private
cpp复制struct MyStruct {
int a; // 默认public
};
class MyClass {
int b; // 默认private
};
1.3 成员属性私有化的优点
将成员属性设置为私有(private)有以下几个优点:
- 可以自己控制读写权限
- 对于写操作,可以检测数据的有效性
- 提高了代码的安全性和可维护性
cpp复制#include<iostream>
#include<string>
using namespace std;
class Person {
public:
// 设置姓名(可写)
void setName(string name) {
m_Name = name;
}
// 获取姓名(可读)
string getName() {
return m_Name;
}
// 设置年龄(带有效性验证)
void setAge(int age) {
if(age < 0 || age > 150) {
cout << "年龄" << age << "输入有误,赋值失败" << endl;
m_Age = 18; // 默认值
return;
}
m_Age = age;
}
// 获取年龄(只读)
int getAge() {
return m_Age;
}
// 设置偶像(只写)
void setIdol(string idol) {
m_Idol = idol;
}
private:
string m_Name; // 可读可写
int m_Age; // 只读(通过setAge验证)
string m_Idol; // 只写
};
int main() {
Person p;
p.setName("张三");
cout << "姓名:" << p.getName() << endl;
p.setAge(200); // 会提示输入有误,年龄保持默认值18
cout << "年龄:" << p.getAge() << endl;
p.setIdol("杨幂");
// cout << p.m_Idol << endl; // 错误:私有成员,外部无法读取
return 0;
}
实操心得:
- 在实际项目中,建议将所有成员变量设为private,然后根据需要提供public的get/set方法
- 在set方法中加入数据验证逻辑,可以避免非法数据进入系统
- 对于只读属性,只提供get方法;对于只写属性,只提供set方法
2. 类与对象的高级特性
2.1 构造函数与析构函数
2.1.1 构造函数的基本概念
构造函数是一种特殊的成员函数,它在创建对象时自动调用,用于初始化对象的状态。构造函数的特点:
- 没有返回值,也不写void
- 函数名与类名相同
- 可以有参数,可以重载
- 程序在创建对象时自动调用,且只调用一次
cpp复制#include<iostream>
using namespace std;
class Person {
public:
// 构造函数
Person() {
cout << "Person的构造函数调用" << endl;
}
// 析构函数
~Person() {
cout << "Person的析构函数调用" << endl;
}
};
void test() {
Person p; // 栈上的对象,test()执行完会自动销毁
}
int main() {
test();
Person p; // main函数中的对象,程序结束时销毁
system("pause");
return 0;
}
2.1.2 构造函数的分类与调用
构造函数可以按参数分为:无参构造(默认构造)和有参构造;按类型分为:普通构造和拷贝构造。
有三种调用方式:
- 括号法
- 显示法
- 隐式转换法
cpp复制#include<iostream>
using namespace std;
class Person {
public:
// 无参构造
Person() {
cout << "无参构造函数调用" << endl;
}
// 有参构造
Person(int a) {
age = a;
cout << "有参构造函数调用" << endl;
}
// 拷贝构造
Person(const Person &p) {
age = p.age;
cout << "拷贝构造函数调用" << endl;
}
~Person() {
cout << "析构函数调用" << endl;
}
int age;
};
void test() {
// 1. 括号法
Person p1; // 无参构造
Person p2(10); // 有参构造
Person p3(p2); // 拷贝构造
// 2. 显示法
Person p4 = Person(); // 无参
Person p5 = Person(10); // 有参
Person p6 = Person(p5); // 拷贝
Person(10); // 匿名对象,当前行执行完立即销毁
// 3. 隐式转换法
Person p7 = 10; // 相当于 Person p7 = Person(10);
Person p8 = p7; // 拷贝构造
}
int main() {
test();
system("pause");
return 0;
}
注意事项:
- 调用默认构造函数时不要加括号:
Person p1()会被编译器认为是函数声明- 不要用拷贝构造函数初始化匿名对象:
Person(p3)会导致重定义错误- 匿名对象的生命周期只在当前行,执行完立即调用析构函数
2.1.3 拷贝构造函数的调用时机
拷贝构造函数在以下三种情况下会被调用:
- 使用一个已创建的对象初始化新对象
- 值传递的方式给函数参数传值
- 以值方式返回局部对象
cpp复制#include<iostream>
using namespace std;
class Person {
public:
Person() {
cout << "默认构造" << endl;
}
Person(int age) : m_Age(age) {
cout << "有参构造" << endl;
}
Person(const Person &p) : m_Age(p.m_Age) {
cout << "拷贝构造" << endl;
}
~Person() {
cout << "析构" << endl;
}
int m_Age;
};
// 1. 用已创建对象初始化新对象
void test1() {
Person p1(20);
Person p2(p1); // 拷贝构造
}
// 2. 值传递给函数参数
void doWork(Person p) {
cout << "doWork: " << p.m_Age << endl;
}
void test2() {
Person p(30);
doWork(p); // 调用拷贝构造
}
// 3. 值方式返回局部对象
Person doWork2() {
Person p1(40);
cout << (int*)&p1 << endl;
return p1; // 调用拷贝构造
}
void test3() {
Person p = doWork2();
cout << (int*)&p << endl;
}
int main() {
cout << "test1:" << endl;
test1();
cout << "\ntest2:" << endl;
test2();
cout << "\ntest3:" << endl;
test3();
return 0;
}
2.2 深拷贝与浅拷贝
2.2.1 浅拷贝的问题
浅拷贝是简单的值拷贝,当类中有指针成员并在堆区分配内存时,浅拷贝会导致两个对象的指针成员指向同一块内存。这会在析构时引发问题,因为同一块内存会被释放两次。
cpp复制#include<iostream>
using namespace std;
class Person {
public:
Person(int age, int height) {
m_Age = age;
m_Height = new int(height); // 堆区分配
cout << "有参构造" << endl;
}
~Person() {
// 释放堆区数据
if(m_Height != NULL) {
delete m_Height;
m_Height = NULL;
}
cout << "析构" << endl;
}
int m_Age;
int *m_Height; // 身高指针
};
void test() {
Person p1(18, 180);
Person p2(p1); // 默认浅拷贝
cout << "p1年龄:" << p1.m_Age << " 身高:" << *p1.m_Height << endl;
cout << "p2年龄:" << p2.m_Age << " 身高:" << *p2.m_Height << endl;
}
int main() {
test();
return 0;
}
运行这个程序会导致运行时错误,因为p1和p2的m_Height指向同一块内存,析构时会被释放两次。
2.2.2 实现深拷贝
解决方法是自己实现拷贝构造函数,进行深拷贝:
cpp复制class Person {
public:
// ... 其他代码同上 ...
// 深拷贝构造函数
Person(const Person &p) {
m_Age = p.m_Age;
// 深拷贝操作:重新申请堆区内存
m_Height = new int(*p.m_Height);
cout << "拷贝构造(深拷贝)" << endl;
}
};
void test() {
Person p1(18, 180);
Person p2(p1); // 调用深拷贝构造
cout << "p1年龄:" << p1.m_Age << " 身高:" << *p1.m_Height << endl;
cout << "p2年龄:" << p2.m_Age << " 身高:" << *p2.m_Height << endl;
// 修改p2的身高不影响p1
*p2.m_Height = 175;
cout << "修改后:" << endl;
cout << "p1身高:" << *p1.m_Height << endl; // 180
cout << "p2身高:" << *p2.m_Height << endl; // 175
}
int main() {
test();
return 0;
}
经验总结:
- 如果类中有指针成员并在堆区分配内存,必须自己实现拷贝构造函数进行深拷贝
- 深拷贝会为每个对象创建独立的内存空间,避免多个对象共享同一资源
- 在析构函数中要正确释放堆区内存,避免内存泄漏
2.3 初始化列表与静态成员
2.3.1 初始化列表语法
C++提供了初始化列表语法,用于在构造函数中初始化成员变量:
cpp复制class Person {
public:
// 传统初始化方式
// Person(int a, int b, int c) {
// m_A = a;
// m_B = b;
// m_C = c;
// }
// 初始化列表方式
Person(int a, int b, int c) : m_A(a), m_B(b), m_C(c) {}
int m_A;
int m_B;
int m_C;
};
void test() {
Person p(10, 20, 30);
cout << p.m_A << " " << p.m_B << " " << p.m_C << endl;
}
初始化列表的特点:
- 效率更高,特别是对于const成员和引用成员
- 必须用于初始化const成员和引用成员
- 成员初始化的顺序与它们在类中声明的顺序一致,与初始化列表中的顺序无关
2.3.2 静态成员
静态成员属于类而不是对象,所有对象共享同一份静态成员。
静态成员变量:
- 所有对象共享同一份数据
- 在编译阶段分配内存(全局区)
- 类内声明,类外初始化
静态成员函数:
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
cpp复制#include<iostream>
using namespace std;
class Person {
public:
static int m_A; // 静态成员变量声明
static void func() { // 静态成员函数
cout << "static void func调用" << endl;
m_A = 100; // 可以访问静态成员
// m_B = 200; // 错误:不能访问非静态成员
}
int m_B;
private:
static int m_C; // 私有静态成员
static void func2() { cout << "static void func2调用" << endl; }
};
// 静态成员变量初始化
int Person::m_A = 0;
int Person::m_C = 0;
void test() {
// 1. 通过对象访问
Person p;
cout << p.m_A << endl;
// 2. 通过类名访问
cout << Person::m_A << endl;
Person::func();
// Person::m_C; // 错误:私有静态成员不能访问
// Person::func2(); // 错误:私有静态函数不能访问
}
int main() {
test();
return 0;
}
注意事项:
- 静态成员函数没有this指针,因此不能访问非静态成员
- 静态成员变量必须在类外初始化(分配内存)
- 静态成员也有访问权限(public/protected/private)
3. 类的高级应用与设计案例
3.1 类对象作为类成员
当一个类的成员是另一个类的对象时,我们称该成员为对象成员。这种情况下,构造和析构的顺序如下:
- 构造顺序:先构造成员对象,再构造自身
- 析构顺序:与构造顺序相反,先析构自身,再析构成员对象
cpp复制#include<iostream>
#include<string>
using namespace std;
class Phone {
public:
Phone(string name) : m_Name(name) {
cout << "Phone构造:" << m_Name << endl;
}
~Phone() {
cout << "Phone析构:" << m_Name << endl;
}
string m_Name;
};
class Person {
public:
Person(string name, string phone) : m_Name(name), m_Phone(phone) {
cout << "Person构造:" << m_Name << endl;
}
~Person() {
cout << "Person析构:" << m_Name << endl;
}
string m_Name;
Phone m_Phone;
};
void test() {
Person p("张三", "iPhone13");
cout << p.m_Name << "拿着" << p.m_Phone.m_Name << endl;
}
int main() {
test();
return 0;
}
输出结果:
code复制Phone构造:iPhone13
Person构造:张三
张三拿着iPhone13
Person析构:张三
Phone析构:iPhone13
3.2 综合设计案例:点和圆的关系
让我们通过一个综合案例来应用前面学到的知识:判断一个点是否在圆内、圆上或圆外。
3.2.1 类的设计
我们需要设计两个类:
- Point类:表示点,包含x和y坐标
- Circle类:表示圆,包含半径和圆心(Point对象)
cpp复制// point.h
#pragma once
#include<iostream>
using namespace std;
class Point {
public:
void setX(int x);
int getX();
void setY(int y);
int getY();
private:
int m_X;
int m_Y;
};
// circle.h
#pragma once
#include "point.h"
class Circle {
public:
void setR(int r);
int getR();
void setCenter(Point center);
Point getCenter();
private:
int m_R;
Point m_Center;
};
3.2.2 类的实现
cpp复制// point.cpp
#include "point.h"
void Point::setX(int x) { m_X = x; }
int Point::getX() { return m_X; }
void Point::setY(int y) { m_Y = y; }
int Point::getY() { return m_Y; }
// circle.cpp
#include "circle.h"
void Circle::setR(int r) { m_R = r; }
int Circle::getR() { return m_R; }
void Circle::setCenter(Point center) { m_Center = center; }
Point Circle::getCenter() { return m_Center; }
3.2.3 判断点与圆的关系
cpp复制#include<iostream>
#include "circle.h"
#include "point.h"
// 判断点和圆的关系
void isInCircle(Circle &c, Point &p) {
// 计算两点距离平方
int distance =
(c.getCenter().getX() - p.getX()) * (c.getCenter().getX() - p.getX()) +
(c.getCenter().getY() - p.getY()) * (c.getCenter().getY() - p.getY());
// 计算半径平方
int rDistance = c.getR() * c.getR();
// 判断关系
if(distance == rDistance) {
cout << "点在圆上" << endl;
}
else if(distance > rDistance) {
cout << "点在圆外" << endl;
}
else {
cout << "点在圆内" << endl;
}
}
int main() {
// 创建圆
Circle c;
c.setR(10);
Point center;
center.setX(10);
center.setY(0);
c.setCenter(center);
// 创建点
Point p;
p.setX(10);
p.setY(9);
// 判断关系
isInCircle(c, p); // 输出:点在圆内
return 0;
}
3.3 综合设计案例:立方体类
再来看一个立方体类的设计案例,要求:
- 设计立方体类(Cube)
- 设计属性:长、宽、高
- 设计行为:获取表面积和体积
- 分别用全局函数和成员函数判断两个立方体是否相等
cpp复制#include<iostream>
using namespace std;
class Cube {
public:
// 设置长
void setL(int l) { m_L = l; }
int getL() { return m_L; }
// 设置宽
void setW(int w) { m_W = w; }
int getW() { return m_W; }
// 设置高
void setH(int h) { m_H = h; }
int getH() { return m_H; }
// 计算表面积
int calculateS() {
return 2 * (m_L*m_W + m_L*m_H + m_W*m_H);
}
// 计算体积
int calculateV() {
return m_L * m_W * m_H;
}
// 成员函数判断是否相等
bool isSame(Cube &c) {
return m_L == c.getL() && m_W == c.getW() && m_H == c.getH();
}
private:
int m_L;
int m_W;
int m_H;
};
// 全局函数判断是否相等
bool isSame(Cube &c1, Cube &c2) {
return c1.getL() == c2.getL() &&
c1.getW() == c2.getW() &&
c1.getH() == c2.getH();
}
void test() {
Cube c1;
c1.setL(10);
c1.setW(10);
c1.setH(10);
cout << "c1表面积:" << c1.calculateS() << endl; // 600
cout << "c1体积:" << c1.calculateV() << endl; // 1000
Cube c2;
c2.setL(10);
c2.setW(10);
c2.setH(10);
// 全局函数判断
cout << "全局函数判断:" << isSame(c1, c2) << endl;
// 成员函数判断
cout << "成员函数判断:" << c1.isSame(c2) << endl;
}
int main() {
test();
return 0;
}
设计经验:
- 将数据成员设为private,通过public方法访问,提高安全性
- 成员函数可以直接访问私有成员,而全局函数需要通过公有接口访问
- 比较对象相等性时,通常需要比较所有相关属性
- 计算类的方法(如calculateS)通常设为const成员函数,因为它们不修改对象状态
4. 常见问题与最佳实践
4.1 构造函数调用规则
C++编译器默认会为一个类提供以下函数:
- 默认构造函数(无参,空实现)
- 默认析构函数(无参,空实现)
- 默认拷贝构造函数(值拷贝)
构造函数调用规则:
- 如果用户定义了有参构造函数,C++不再提供默认无参构造,但会提供默认拷贝构造
- 如果用户定义了拷贝构造函数,C++不再提供其他构造函数
cpp复制#include<iostream>
using namespace std;
class Person {
public:
// 如果只定义有参构造,编译器不会提供默认构造
Person(int age) {
m_Age = age;
cout << "有参构造" << endl;
}
// 如果定义了拷贝构造,编译器不会提供其他构造
// Person(const Person &p) {
// m_Age = p.m_Age;
// cout << "拷贝构造" << endl;
// }
~Person() {
cout << "析构" << endl;
}
int m_Age;
};
void test() {
// Person p1; // 错误:没有默认构造函数
Person p2(10); // 调用有参构造
Person p3(p2); // 调用拷贝构造(编译器提供)
}
int main() {
test();
return 0;
}
4.2 成员变量初始化最佳实践
在实际开发中,成员变量的初始化有以下几种方式:
- 构造函数体内赋值:
cpp复制Person(int a, int b) {
m_A = a;
m_B = b;
}
- 初始化列表:
cpp复制Person(int a, int b) : m_A(a), m_B(b) {}
- C++11类内初始化:
cpp复制class Person {
int m_A = 0; // 类内初始化
int m_B = 0;
Person(int a, int b) : m_A(a), m_B(b) {}
};
最佳实践建议:
- 对于简单类型(int、float等),使用初始化列表或类内初始化
- 对于const成员和引用成员,必须使用初始化列表
- 对于类类型成员,如果构造需要参数,使用初始化列表
- 初始化顺序应与成员声明顺序一致,避免依赖问题
4.3 类设计中的常见陷阱
-
浅拷贝问题:
当类中有指针成员并在堆区分配内存时,必须自己实现拷贝构造函数进行深拷贝,否则会导致重复释放内存等问题。 -
循环依赖:
当两个类互相包含对方的对象或指针时,会导致循环依赖。解决方法:- 使用前向声明
- 将包含关系改为指针或引用
-
静态成员初始化:
静态成员变量必须在类外初始化(分配内存),否则会导致链接错误。 -
const成员初始化:
const成员变量必须在初始化列表中初始化,不能在构造函数体内赋值。 -
虚析构函数:
当类可能被继承时,应将析构函数声明为虚函数,确保通过基类指针删除派生类对象时能正确调用派生类的析构函数。
cpp复制// 循环依赖示例
// a.h
#pragma once
#include "b.h"
class A {
B b; // 错误:B尚未完全定义
};
// 正确做法:使用前向声明和指针
// a.h
#pragma once
class B; // 前向声明
class A {
B *b; // 使用指针
};
4.4 性能优化建议
-
尽量使用初始化列表:
初始化列表比构造函数体内赋值效率更高,特别是对于类类型成员。 -
避免不必要的拷贝:
- 对于大对象,使用const引用传递参数
- 使用移动语义(C++11)转移资源所有权
-
内联简单成员函数:
对于简单的get/set函数,可以在类定义中直接实现,编译器会自动内联。 -
使用对象池:
对于频繁创建销毁的对象,可以考虑使用对象池技术减少内存分配开销。 -
避免虚函数滥用:
虚函数调用有额外开销,只在必要时使用虚函数。
cpp复制// 避免不必要拷贝的例子
void processPerson(Person p); // 值传递,会产生拷贝
void processPerson(const Person &p); // 引用传递,无拷贝
// 移动语义示例(C++11)
Person createPerson() {
Person p;
// ... 初始化p ...
return p; // C++11会使用移动语义而非拷贝
}
通过合理应用这些面向对象编程技术和最佳实践,可以构建出结构清晰、安全高效、易于维护的C++程序。记住,良好的类设计是高质量软件的基础,在实际开发中应该根据具体需求灵活运用这些概念和技术。