C++ 类和对象
类和对象
三大特性:
- 封装
- 继承
- 多态
封装
类的访问权限
权限 | 描述 |
---|---|
public | 类内可以访问,类外可以访问 |
protected | 类内可以访问,类外不可访问,后代可以访问 |
private | 类内可以访问,类外不可访问,后代不可访问 |
struct 和 class 区别
struct 默认权限为 public,class 默认权限为 private.
成员属性私有化
- 可以控制读写权限
- 可以对数据有效性进行检验
#include <string>
#include <iostream>
using namespace std;
class Person {
private:
string name;
public:
string getName() {
return name;
}
void setName(string name) {
this->name = name;
}
};
int main() {
Person p;
p.setName("张三");
cout << "姓名为:" << p.getName() << endl;
return 0;
}
对象初始化和清理
构造函数和析构函数
类的构造函数会在每次创建类的新对象时执行,析构函数会在每次删除所创建的对象时执行。
若没有提供构造函数和析构函数,编译器会自动提供空的构造函数和析构函数。
#include <iostream>
using namespace std;
class Person {
public:
Person() {
cout << "构造函数被调用" << endl;
}
~Person() {
cout << "析构函数被调用" << endl;
}
};
int main() {
{
Person p;
}
system("pause");
return 0;
}
构造函数
语法:
classname() {
...
}
注意:
- 函数名与类名相同
- 没有返回
- 可以有参数,可以重载
析构函数
语法
~classname() {
...
}
- 函数名与类名相同,且名称前有
~
符号 - 没有返回
- 不可以有参数,不可以重载
构造函数分类
按照是否有参数分类
- 无参构造
- 有参构造
按照类型分类
- 普通构造函数
- 拷贝构造函数
构造函数调用
- 隐式调用(
Person p(10)
) - 显式调用(
Person p = Person(10);
) - 隐式转换调用(
Person p = 10
)
#include <iostream>
using namespace std;
class Person {
public:
Person() {
this->age = 0;
cout << "无参构造函数被调用" << endl;
}
Person(int age) {
this->age = age;
cout << "有参构造函数被调用" << endl;
}
Person(const Person& p) {
this->age = p.age;
cout << "拷贝构造函数被调用" << endl;
}
~Person() {
cout << "析构函数被调用" << endl;
}
int getAge() {
return this->age;
}
void setAge(int age) {
this->age = age;
}
private:
int age;
};
// 构造函数调用的三种方法
void test1() {
// 隐式调用构造函数
Person p1;
Person p2(10);
Person P3(p2);
}
void test2() {
// 显式调用构造函数
Person p1;
Person p2 = Person(10);
Person p3 = Person(p2);
}
void test3() {
// 隐式转换调用构造函数
Person p4 = 10;
Person p5 = p4;
}
int main() {
test1();
test2();
test3();
return 0;
}
拷贝构造函数调用时机
- 用已创建的对象初始化一个新对象
- 函数调用时,以值传递方式给函数参数传参
- 函数以值方式返回局部对象
构造函数调用规则
默认情况下,C++ 编译器会给一个类自动生成
- 默认构造函数(空,无参)
- 默认析构函数(空,无参)
- 默认拷贝构造函数(值拷贝 )
调用规则
- 若用户定义了有参构造函数,编译器将不生成默认无参构造函数,但会生成默认拷贝构造函数
- 若用户定义了拷贝构造函数,编译器将不会生成其他构造函数
深拷贝与浅拷贝
深拷贝:在堆内存重新申请空间,进行拷贝操作
浅拷贝:简单赋值拷贝
默认的拷贝构造函数是进行浅拷贝,因此在类构造时,涉及申请内存时,可能会在析构时对同一个内存释放两次,造成错误。
如果构造函数使用了 new,则必须提供使用 delete 的析构函数。
#include <iostream>
using namespace std;
class Person {
public:
Person() {
age = 0;
hight = new int(160);
}
Person(int age, int hight) {
this->age = age;
this->hight = new int(hight);
}
// 将下面的拷贝构造函数注释,程序会崩
Person(const Person& p) {
age = p.age;
hight = new int(*p.hight);
cout << "拷贝构造函数被调用" << endl;
}
~Person() {
cout << "析构函数被调用" << endl;
if (hight != NULL) {
delete hight;
hight = NULL;
}
}
int age;
int* hight;
};
void test() {
Person p1(18, 160);
cout << "p1 身高:" << *p1.hight << endl;
Person p2(p1);
cout << "p2 身高:" << *p2.hight << endl;
}
int main() {
test();
return 0;
}
初始化列表
构造函数初始化列表以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化式。例如:
class Person
{
public:
Person(string name, int age):m_name(name),m_age(age){}; // 初始化列表
private:
string m_name;
int m_age;
};
类对象作为类成员
C++ 中的成员可以是另一个类对象。
当 A 类作为 B 类的成员,构造时,先调用 A 类的构造函数,再调用 B 类的构造函数。析构时,顺序相反。
例如:
class A{}
class B{
A a;
}
静态成员变量
- 所有对象共享一个静态成员变量
- 在编译阶段分配内存
- 类内声明,类外初始化
静态成员函数
- 所有对象共享一个函数
- 静态成员函数只能访问静态成员变量,不能访问非静态成员变量
class Person
{
public:
static void func(){
cout << "调用静态成员函数"
}
}
// 调用
void test(){
// 通过对象访问
Person p;
p.func();
// 通过类名访问
Person::func();
}
C++ 对象模型和 this 指针
成员变量和成员函数
- 成员变量和成员函数分开存储
- 静态成员变量不存储在类中
- 非静态成员函数不存储在类中(存储在类中的只有非静态成员变量)
- 空对象占用 1 个字节(为了区分其他对象)
this 指针
this 指针指向调用它的成员函数所属的对象。
用途:
- 形参和成员变量同名,可以用 this 指针区分 (也可以在成员变量前加
m_
) - 返回对象本身(
return *this;
)
空指针访问成员函数
空指针可以访问成员函数
#include <string>
#include <iostream>
using namespace std;
class Person {
public:
string name;
void showPersonName() {
// 可以添加判断 this 是否为 NULL 的代码增强健壮性
cout << this->name << endl;
}
void sayHi() {
cout << "Hi" << endl;
}
};
int main() {
Person* p_ptr = NULL;
p_ptr->sayHi(); // 不会报错
// p_ptr->showPersonName(); // 报错
return 0;
}
const 修饰成员函数
常函数:
- 函数末尾有
const
关键字 - 常函数不可以修改成员属性
- 成员属性声明时加
mutable
关键字,则在常函数中可以修改
常对象:
- 声明对象前加
const
的对象 - 常对象只能调用常函数
示例:
#include <iostream>
using namespace std;
class Person
{
public:
void setInfo() const {
m_a = 0; // 报错:无法修改
m_b = 0; // 可以修改
};
void func(){};
int m_a;
mutable int m_b;
};
// 测试常函数
void test1(){
Person p;
p.setInfo();
};
// 测试常对象
void test2(){
const Person p;
p.func(); // 报错: 无法修改
};
int main()
{
return 0;
}
友元
程序中,需要让类外的函数或类访问私有属性,需要用到友元技术,关键字为 friend
。
友元的的三种实现
- 全局函数做友元
- 类做友元
- 成员函数做友元
示例:
#include <iostream>
#include <string>
using namespace std;
class Building;
class FriendClass2 {
public:
FriendClass2();
void visit();
Building* building;
};
class Building {
// 使全局函数可以访问私有成员
friend void friendFunc(Building& building);
// 使类可以访问私有成员
friend class FriendClass;
// 使类成员函数可以访问私有成员
friend void FriendClass2::visit();
public:
Building(string sittingroom = "客厅", string bedroom = "卧室"):
m_sittingroom(sittingroom), m_bedroom(bedroom) {};
string m_sittingroom;
private:
string m_bedroom;
};
void friendFunc(Building& building) {
cout << "全局函数访问:" << building.m_sittingroom << endl;
cout << "全局函数访问:" << building.m_bedroom << endl;
}
class FriendClass {
public:
void visit(Building& building) {
cout << "类访问:" << building.m_sittingroom << endl;
cout << "类访问:" << building.m_bedroom << endl;
}
};
FriendClass2::FriendClass2() {
building = new Building;
}
void FriendClass2::visit() {
cout << "类成员函数访问:" << building->m_sittingroom << endl;
cout << "类成员函数访问:" << building->m_bedroom << endl;
}
int main()
{
Building building;
// 全局函数做友元
friendFunc(building);
// 类做友元
FriendClass fc;
fc.visit(building);
// 类成员函数
FriendClass2 fc2;
fc2.visit();
}
运算符重载
重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。
一般通过类内成员函数或友元函数重载运算符,下面的示例定义了一个 Time
类,并重载了 +
、-
、<<
运算符。
重载 +
、-
、<<
运算符
示例:
#include <iostream>
using namespace std;
class Time {
public:
// 构造函数
Time(int min = 0, int second = 0) :
m_min(min), m_second(second) {}
// 重载 << 运算符用于输出
friend ostream& operator << (ostream& os, const Time& t) {
os << t.m_min << " m " << t.m_second << " s ";
return os;
}
// 通过成员函数重载 + 运算符
// 相当于 t1.opeator+(t2)
Time operator+ (const Time& t) {
Time temp;
temp.m_min = this->m_min + t.m_min + (this->m_second + t.m_second) / 60;
temp.m_second = (this->m_second + t.m_second) % 60;
return temp;
}
// 通过友元函数重载 - 运算符
// 相当于 opeator-(t1, t2)
friend Time operator- (Time& t1, const Time t2) {
// 演示没有实现进退维
Time t;
t.m_min = t1.m_min - t2.m_min;
t.m_second = t1.m_second - t2.m_second;
return t;
}
private:
int m_min;
int m_second;
};
void testAdd(){
Time t1(1, 50);
Time t2(2, 30);
Time t3 = t1 + t2;
cout << "t1 is " << t1 << endl;
cout << "t2 is " << t2 << endl;
cout << "t1 + t2 is " << t3 << endl;
}
void testSub() {
Time t1(1, 10);
Time t2(2, 30);
Time t3 = t1 - t2;
cout << "t1 is " << t1 << endl;
cout << "t2 is " << t2 << endl;
cout << "t1 - t2 is " << t3 << endl;
}
int main()
{
testAdd();
testSub();
}
重载递增运算符(++
)
重载 ++
运算符
- 编译器通过判断运算符重载函数参数中是否插入关键字
int
来区分前置递增和后置递增 Type& operator++()
:前置递增Type operator++(int)
:后置递增
示例:重载递增运算符
#include <iostream>
using namespace std;
class MyInteger {
public:
MyInteger() {
m_num = 0;
}
friend ostream& operator<< (ostream& os, MyInteger myint) {
os << myint.m_num;
return os;
}
// 重载前置 ++ 运算符
MyInteger& operator++ () {
m_num++;
return *this;
}
// 重载后置 ++ 运算符 (注意返回不是引用)
MyInteger operator++ (int) {
// 记录当前结果
MyInteger temp = *this;
// 后自增
m_num++;
// 返回记录结果
return temp;
}
private:
int m_num;
};
void testFrontSelfIncrement() {
MyInteger myint;
cout << ++myint << endl;
cout << ++(++myint) << endl;
cout << myint << endl;
}
void testEndSelfIncrement() {
MyInteger myint;
cout << myint++ << endl;
cout << (myint++)++ << endl;
cout << myint << endl;
}
int main()
{
testFrontSelfIncrement();
testEndSelfIncrement();
}
重载赋值运算符(=
)
赋值运算符对属性进行值拷贝。使用默认的赋值运算符,若在构造函数中申请了内存,在析构时可能造成堆区内存重复释放,造成程序崩溃。
下面的代码会崩溃,原因是内存重复释放。
#include <iostream>
using namespace std;
class Person {
public:
int* m_age;
Person(int age) {
m_age = new int(age);
}
~Person() {
if (m_age != NULL) {
delete m_age;
m_age = NULL;
}
}
};
void test() {
Person p1(18);
Person p2(20);
p2 = p1;
cout << "p1 的年龄为:" << *p1.m_age << endl;
}
int main()
{
test();
}
解决方法是重载 =
运算符
// 11-运算符重载-赋值运算符重载.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
using namespace std;
class Person {
public:
int* m_age;
Person(int age) {
m_age = new int(age);
}
~Person() {
if (m_age != NULL) {
delete m_age;
m_age = NULL;
}
}
Person& operator= (Person &p) {
// 首先判断是否有属性在堆区,若有,需释放内存
if (m_age != NULL) {
delete m_age;
m_age = NULL;
}
// 进行深拷贝
m_age = new int(*p.m_age);
return *this;
}
};
void test() {
Person p1(18);
Person p2(20);
Person p3(30);
p3 = p2 = p1;
cout << "p1 的年龄为:" << *p1.m_age << endl;
cout << "p2 的年龄为:" << *p2.m_age << endl;
cout << "p3 的年龄为:" << *p3.m_age << endl;
}
int main()
{
test();
}
函数调用运算符重载(()
)
使用方法类似函数调用,因此又称为仿函数。
#include <iostream>
#include <string>
using namespace std;
class MyPrint {
public:
void operator()(string s) {
cout << s;
}
};
int main()
{
// 使用匿名对象
MyPrint()("Hello world!");
}
继承
已有一个实现部分功能的类,想要基于已有类新建一个含有新功能(或不同功能)的类,可以继承已有的类,减少代码量。这个已有的类称为基类,新建的类称为派生类。
语法
单继承
// 基类
class Base {
};
//派生类
class Drived : public Base {
};
多继承
// 基类
class Base1;
class Base2;
//派生类
class Drived : public Base1, public Base2 {
};
public
是一种继承方式。开发中不建议使用多继承。
继承方式
- 公共继承
- 保护继承
- 私有继承


继承中的对象模型
父类中所有的非静态成员属性都会被子类继承,私有属性也会被继承,但是被隐藏了。
继承中构造和析构顺序
基类构造 > 派生类构造 > 派生类析构 > 基类析构
继承中同名成员处理方式
派生类访问基类同名成员需要加上基类的作用域 。
同名静态成员处理方式一致。
class A {
public:
int a;
func();
}
Class B: public A{
public:
int a;
func();
}
void test(){
B b;
// 访问自己的 func
b.func();
// 访问基类的 func
b.A::func();
}
菱形继承
两个派生类继承与一个基类,又有某个类同时继承这两个类。
为了解决多继承时的命名冲突和冗余数据问题,C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员。
//间接基类A
class A{
protected:
int m_a;
};
//直接基类B
class B: virtual public A{ //虚继承
protected:
int m_b;
};
//直接基类C
class C: virtual public A{ //虚继承
protected:
int m_c;
};
//派生类D
class D: public B, public C{
public:
void seta(int a){ m_a = a; } //正确
void setb(int b){ m_b = b; } //正确
void setc(int c){ m_c = c; } //正确
void setd(int d){ m_d = d; } //正确
private:
int m_d;
};
int main(){
D d;
return 0;
}
这段代码使用虚继承重新实现了菱形继承,这样在派生类 D 中就只保留了一份成员变量 m_a。
虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的 A 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。
多态
C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。
分类
- 静态多态:运算符重载、函数重载
- 动态多态:使用派生类和虚函数实现
静态多态和动态多态区别:
- 静态多态的函数地址在编译阶段确定(早绑定)
- 动态多态的函数地址在运行阶段确定(晚绑定)
多态的条件
- 有继承关系
- 派生类重写基类的虚函数
使用时,符类指针或引用指向子类对象。
示例:
#include <iostream>
using namespace std;
class Animal {
public:
// 虚函数
virtual void speak() {
cout << "动物在说话" << endl;
}
void eat() {
cout << "动物在吃" << endl;
}
};
class Cat: public Animal {
public:
void speak() {
cout << "喵喵喵" << endl;
}
void eat() {
cout << "猫在吃" << endl;
}
};
class Dog : public Animal {
public:
virtual void speak() {
cout << "汪汪汪" << endl;
}
void eat() {
cout << "狗在吃" << endl;
}
};
void speak(Animal &animal) {
animal.speak();
}
void eat(Animal& animal) {
animal.eat();
}
int main()
{
Cat cat;
speak(cat); // 喵喵喵
eat(cat); // 期望“猫在吃”,实际“动物在吃”
Dog dog;
speak(dog); // 汪汪汪
eat(dog); // 期望“狗在吃”,实际“动物在吃”
}
多态原理
多态的关键在于通过基类指针或引用调用一个虚函数时,编译时不能确定到底调用的是基类还是派生类的函数,运行时才能确定。
每一个有「虚函数」的类(或有虚函数的类的派生类)都有一个「虚函数表」,该类的任何对象中都放着虚函数表的指针(vfptr)。「虚函数表」中列出了该类的「虚函数」地址。
纯虚函数和抽象类
多态中,基类中的虚函数的实现通常是无意义的,主要都是调用子类重写的函数,因此可以将基类中的虚函数写为纯虚函数(未给出有意义实现的函数)
实现纯虚函数的方法是在函数原型后加 =0
:
virtual void func() = 0;
当类中有纯虚函数,这个类就是抽象类。
抽象类特点:
- 无法实例化对象
- 其派生必须重写抽象类中的纯虚函数,否则也属于抽象类
示例:
#include <iostream>
class Base {
public:
virtual void func() = 0;
};
class Drived: public Base {
public:
void func() {
std::cout << "重写了 func()" << std::endl;
}
};
int main()
{
// Base b; // 抽象类无法实例化
Drived d; // 重写了纯虚函数的派生类可以实例化
}
虚析构和纯虚析构
使用多态时,若子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码。
解决方式:将父类中的析构函数改为虚析构或纯虚析构
虚析构和纯虚修够的共性:
- 可以解决父类指针无法释放子类对象的问题
- 都需要有具体实现
区别:
- 若类有纯虚析构,则该类未抽象类,无法实例化对象
示例:
#include <iostream>
#include <string>
using namespace std;
class Animal {
public:
Animal() {
cout << "Animal 构造函数调用" << endl;
}
// 若 Animal 不是虚函数,则 Cat 的析构函数无法被调用
virtual ~Animal() {
cout << "Animal 析构函数调用" << endl;
}
// 若使用下面的纯虚析构,需要具体实现
// virtual ~Animal() = 0;
virtual void speak() = 0;
string* m_name;
};
class Cat : public Animal {
public:
Cat(string name) {
cout << "Cat 构造函数调用" << endl;
m_name = new string(name);
}
void speak() {
cout << *m_name << " 小猫在说话" << endl;
}
~Cat() {
cout << "Cat 析构函数调用" << endl;
if (m_name != NULL) {
delete m_name;
m_name = NULL;
}
}
string* m_name;
};
void test() {
//Animal* cat = new Cat("Tom");
//cat->speak();
//delete cat
Cat cat("Tom");
cat.speak();
}
int main()
{
test();
}
输出:
Animal 构造函数调用
Cat 构造函数调用
Tom 小猫在说话
Cat 析构函数调用
Animal 析构函数调用