文章目录
一、面向对象程序设计概述
1、面向对象的思想
- 面向对象的思想认为,客观世界是由各种各样的对象构成的,每种对象都有各自的
属性和方法
,不同对象之间的相互作用和联系
构成了不同的系统 - 在面向对象的思想中,任何事物都可以被看做一个对象,一个再复杂的模型结构都是
由千千万万个对象组成
的,这是根本思想
2、面向对象的几个特征
- 封装性:
- 把对象的属性和方法结合成一个独立的系统单位,并尽可能隐藏对象的内部细节,外部若想访问,必须通过指定的接口
- 封装是面向对象思想描述的基础,从此程序员不再面对一个个变量和函数,而是要放眼大局,面对一个个对象来看问题
- 通过
public/protected/private
访问修饰符对变量和函数实现访问控制,private
关键字的作用在于更好地隐藏类的内部实现,该向外暴露的接口(能通过对象访问的成员)都声明为public
,不希望外部知道、或者只在类内部使用的、或者对外部没有影响的成员,都建议声明为private
- 继承性:
- 子类自动共享父类属性和方法的机制
- 多态性:
- 在基类中定义的属性和方法被子类继承后,可以具有不同的 数据类型或者表现行为(方法) 等特性
- 可以对一个类不同的实例对象调用相同的方法(继承重载),产生不同的结果
3、怎样生成对象?
- 先定义一个类(从语法上来说,其地位等同于char、int、float、double等基本类型),然后再生成类实例(对象)
- 对象和结构体的区别:对象的内部可以有变量和函数(默认权限是
private
),而结构体通常只由各种变量构成(C++可以有函数,C 中无),默认权限是public
,且结构体一般无默认构造函数和析构函数
二、类和对象
1、类:对一组对象中共性的属性和方法
进行的抽象
- 说明部分:用来声明类的成员函数或成员变量(不能进行初始化)
- 实现部分:用来定义成员函数
- 在类外部实现时:要使用 作用域运算符
::
指明成员函数所属的类,eg:DataStore::DataStore() // 函数定义,要加类名前缀
- 在类内部实现时:不需要加作用域运算符且类外部的定义可以省略
- 在类外部实现时:要使用 作用域运算符
- 访问权限修饰符:
public/protected/private
后面要加冒号,且要顶格书写,不缩进- 使用访问修饰符可以实现类的封装,很明确的告诉用户,哪些是可以调用,哪些是不能调用的,如果没有访问修饰符,则类中的成员函数/变量默认是
private
的 - 在类的内部:无论成员被声明为
public
、protected
还是private
,都是可以互相访问的,没有访问权限的限制 - 在类的外部:只能通过对象访问成员,并且通过对象只能访问
public
属性的成员,不能访问private
、protected
(派生类可以访问) 属性的成员
- 使用访问修饰符可以实现类的封装,很明确的告诉用户,哪些是可以调用,哪些是不能调用的,如果没有访问修饰符,则类中的成员函数/变量默认是
- 类的使用注意事项:
- 类的成员变量不能在声明时进行初始化,只能通过成员函数(构造函数)来实现
自身类的对象不能作为自己的成员,但自身类的指针可以作为自身的成员
- 作用域解析操作符(::),作用是告诉编译器这个方法存在于何处,或者说是属于哪一个类
// 1、封装两层含义
// 把变量(属性)和函数(操作)合成一个整体,封装在一个类中
// 访问控制,现实事物本身有些属性和行为是不对外开放的
class Person {
public:
int mTall; // 多高,可以让外人知道
void dese() {
cout << "我有钱,年轻,个子又高,就爱嘚瑟!" << endl;
}
protected:
int mMoney; // 有多少钱,只能儿子孙子(派生类)知道
private:
int mAge; // 年龄,不想让外人知道
};
// 2、建议将所有成员属性设置为私有
// 使用公共的对外接口 get 或 set 方法来进行读写
// get 或 set 方法中可实现参数的检查
class AccessLevels {
private:
int readOnly; // 对外只读访问(设置相应读访问函数)
int writeOnly; // 对外只写访问(设置相应写访问函数)
int readWrite; // 对外读写访问(设置相应读写访问函数)
int noAccess; // 对外不可访问(不设置相应读写访问函数)
int m_Age = 0; // 类内可以访问,类外可通过函数间接访问
public:
// 只读:对只读属性进行只读访问
int getReadOnly() { return readOnly; }
// 只写:对只写属性进行只写访问
void setWriteOnly(int val) { writeOnly = val; }
// 读写:对读写属性进行读写访问
void setReadWrite(int val) { readWrite = val; }
int getReadWrite() { return readWrite; }
// 设置年龄
void setAge(int age) {
// 参数检查
if (age < 0 || age > 100) {
cout << "wrong age! please reenter!" << endl;
return;
}
m_Age = age;
}
};
2、对象:类的一个实例
- 对象的定义:
类名 对象名;
- 类定义仅仅提供了一种类型的定义,同结构类型一样,从语法上来说,其地位等同于 char、int、float、double等基本类型
- 它本身不占用存储空间,只有在定义了属于类的变量后,系统才会为其分配空间,这种
变量
称为对象 - 对象是类的实例化,存储特定的信息和作用在这些信息之上的特定操作
- 对象成员的访问:
- 通过
对象.
或者类::
访问- 成员变量:
对象名.成员变量名 or 类名::静态成员变量名
- 成员函数:
对象名.成员函数名(实参表) or 类名::静态成员函数名(实参表)
- 成员变量:
- 通过
指针->
或者*指针.
访问:类名* p = &对象名;
- 成员变量:
p->成员变量名,*p.成员变量名;
- 成员函数:
p->成员函数名(实参表),*p.成员函数名(实参表);
- 成员变量:
- 通过
- 对象的初始化和清理:构造函数和析构函数必须写在
public
下,需要析构函数的类也需要拷贝和赋值
操作,需要拷贝操作的类也需要赋值操作,反之亦然- 构造函数:
用于初始化对象,只有在初始化之后,C++对象才是真正意义上的对象
- 构造函数名与类名相同,不能指定函数类型、没有返回值,可带参数,可重载,是成员函数,可在类体内实现,亦可在类体外实现
- 默认构造函数:不需要传参的构造函数,如果没有默认构造函数,则无法创建数组(
Circle* c = new Circle [4]; // 不能传参数,所以需要默认构造函数
) - 参数化构造函数:需要传参的构造函数(或者参数有默认值,则无需传参),创建对象时可以传入参数来改变成员变量的初始值
- 拷贝构造函数:又叫复制构造函数,使用另一个对象初始化本对象,会进行简单的值拷贝(
浅拷贝
)- 如果属性里有指向堆空间的数据,那么简单的浅拷贝会导致析构时重复释放内存的风险
- 解决方法:需要我们自己提供拷贝构造函数,进行
深拷贝(memcpy)
(这样原有对象和新对象所持有的动态内存是相互独立的,更改一个对象的数据不会影响另外一个对象),注意析构函数里面进行内存释放
explicit
关键字:用于修饰构造函数,防止构造函数中的隐式类型转换- Note:在创建对象时,编译器会自动添加并调用缺省空构造函数(包括拷贝构造函数)
- 编译器默认给一个类提供 3个函数: 默认构造函数 、 拷贝构造函数 、 析构函数
- 如果用户定义了拷贝构造函数,编译器不会再提供任何默认构造函数 ,一般需要同时提供拷贝赋值函数
- 如果用户定义了普通构造函数(非拷贝),编译器不再提供默认无参构造函数,但是会提供默认拷贝构造
- 析构函数:
对象在销毁(超出对象作用域)之前,做一个清理善后工作(eg: 释放堆栈等内存、关闭文件)
- 析构函数名与类名相同(但要在其前面加上波浪线
~
),不能指定函数类型,不能带参数,没有返回值,且不允许重载 - 在对象被销毁时,编译器会自动添加并调用缺省空析构函数(若代码中提供,则使用代码中的析构函数)
- 析构函数名与类名相同(但要在其前面加上波浪线
- 类对象作为成员:
- 当类对象作为类成员的时候,构造顺序是先调用类对象成员的构造函数,然后调用自己的构造函数
- 析构顺序与构造相反
class Circle { public: int x; // 类的数据成员:不能在声明时进行初始化,只能通过成员函数来实现(一般用构造函数来实现) int y; int radius; int datasize; char* data; public: // 以下构造函数只需提供一个即可,一般带参构造函数最常用 // 无参构造函数 Circle() // 构造函数:初始化类的数据成员 { x = 0; y = 0; radius = 1; printf("构造函数 \n"); } // 一般构造函数:内部赋值方式(可以部分参数无参形式给出) Circle(int x0, int y0, int r0) // 构造函数重载 { this->x = x0; // this-> 也可以不要 this->y = y0; this->radius = r0; printf("构造函数重载 \n"); } // 一般构造函数:初始化列表方式(效率更高一些),以一个冒号开始,接着是以逗号分隔的数据成员列表, // 每个数据成员后面跟一个放在括号中的初始化值 Circle(int x0, int y0, int r0) : x(x0), y(y0), radius(r0) // 构造函数重载 { printf("构造函数重载 \n"); } // 默认构造函数:不需要传参,已经有了默认参数 Circle(int x0 = 0, int y0 = 0, int r0 = 1) { this->x = x0; // this-> 也可以不要 this->y = y0; this->radius = r0; printf("默认构造函数 \n"); } // 拷贝构造函数:使用另一对象初始化本对象(引用,别名) // 常对象的引用 const:确保拷贝构造函数不会更改传入的参数 Circle(const Circle &circle) { x = circle.x + 1; y = circle.y + 1; radius = circle.radius + 1; // 深拷贝,我们需要数据的副本,而不是指向另一个实例数据的指针 data = new char[datasize] strcpy(data, circle.data) printf("拷贝构造函数 %d %d %d\n", x, y, radius); } // 析构函数 ~Circle() { delete[] data; printf("析构函数调用 \n"); } }; int main() { Circle a; // 默认构造函数 Circle b(1, 1, 4); // 默认构造函数:实参覆盖默认初始化参数 Circle c(b); // 拷贝构造函数:浅拷贝,可实现成员变量的复制或更改; // 当对象通过值传递给函数时也会调用拷贝构造函数(如果参数声明为:const Circle & 则不会调用拷贝构造函数) printf("默认拷贝构造函数 %d %d %d \n", c.x, c.y, c.radius); return 0; } ------------------------------------------------------------------------------- // 构造函数和析构函数.h &&.cpp 分离式写法 // 函数声明:写在 .h 头文件中 class DataStore { public: DataStore(); // 构造函数 ~DataStore(); // 析构函数 }; // 函数定义,要加类名前缀:写在 .cpp 源文件中 DataStore::DataStore() { int m_buffer = new int [1024]; // 申请内存 } DataStore::~DataStore() { delete [] m_buffer; // 释放内存 }
- 构造函数:
3、this 指针
-
类中的成员变量和成员函数是分开存储的,只有非静态成员变量是跟类绑定的,即成员变量在堆或栈区分配内存,成员函数在代码区分配内存,
对象所占用的内存仅仅包含了成员变量
-
每一个非内联成员函数只会生成一个函数实例(即多个同类型的对象会
共用一块函数代码
),编译器通过 this 指针指向被调用的成员函数所属的对象来区分哪个对象在调用函数
-
成员函数被调用时:
- 编译器默认地为每个成员函数传入一个
this 指针
,指向了这个对象本身,this 实际上是成员函数的一个形参
,在调用成员函数时将对象的地址作为实参传递给 this - 相当于在成员函数的参数中定义了
void Test(类名* this = 实例对象的地址) // *this=实例对象
- 类中的成员函数访问成员变量时:
this->
是可以省略的,编译器会自己给它加上this->
- Note: 非静态的成员函数才有
this
指针
- 编译器默认地为每个成员函数传入一个
#include<iostream>
using namespace std;
class Person {
public:
Person(int age) {
this->age = age; // this 可以解决命名冲突
}
void compareAge(Person &p) {
if (this->age == p.age) {
cout << "年龄相等" << endl;;
} else {
cout << "年龄不相等" << endl;;
}
}
Person &PlusAge(Person &p) {
this->age += p.age;
return *this; // *this 指向对象本体
}
int age;
};
void test() {
Person p1(10);
Person p2(10);
cout << "p1 的年龄 " << p1.age << endl; // 10
cout << "p2 的年龄 " << p2.age << endl; // 10
p1.compareAge(p2);
p1.PlusAge(p2).PlusAge(p2).PlusAge(p2); //链式编程
cout << "p1 的年龄 " << p1.age << endl; // 40
}
int main() {
test();
return 0;
}
4、静态成员
- 静态成员的提出是为了解决数据共享的问题,它比全局变量在实现数据共享时更为安全,是实现同类多个对象数据共享的好方法。在类中,分为静态数据成员和静态成员函数。在定义或说明时前面需要加关键字 static
- 静态成员变量:
- 是类而不是对象的成员(在编译阶段就分配好了内存,此时对象还未创建),它被所有对象所共享,在内存中只存储一次
- 在类中声明,初始化在
类外进行
,格式为:数据类型 类名::静态数据成员 = 值;
可以通过类名或者对象名来调用
- 静态成员函数:
- 在类中进行实现,可以通过类名或者对象名来调用
- 静态成员函数,编译器不会为它增加形参
this
,不知道将会指向哪个对象,所以无法访问对象的普通成员变量,只能访问静态成员变量 - 普通成员函数,编译器会隐式地增加一个形参
this
,可以访问普通成员变量,也可以访问静态成员变量 - 静态成员函数的意义,不在于信息共享,数据沟通,而在于管理静态数据成员,完成对静态数据成员的封装
static
成员本质上就是全局变量/函数,和类的普通成员变量和成员函数没什么关系
#include<iostream>
using namespace std;
class Person {
public:
static int m_Age;
int m_A;
// 静态成员函数,不可以访问普通成员变量,但可以访问静态成员变量
static void func() {
//m_A = 10;
m_Age = 100;
cout << "func 调用" << endl;
};
// 普通成员函数 可以访问普通成员变量,也可以访问静态成员变量
void myFunc() {
m_A = 100;
m_Age = 200;
}
private:
static int m_other; // 私有权限在类外不能访问
static void func2() {
cout << "func2 调用" << endl;
}
};
int Person::m_Age = 0; // 类外初始化实现
int Person::m_other = 10;
void test() {
// 1、静态成员变量数据共享
Person p1;
Person p2;
p1.m_Age = 10;
p2.m_Age = 20; // 静态成员函数一般不这么初始化,只为演示静态成员变量在内存中只有一份
cout << "通过对象名访问 m_Age: " << p1.m_Age << endl; // 20
cout << "通过对象名访问 m_Age: " << p2.m_Age << endl; // 20
// 2、通过类名访问静态成员变量
cout << "通过类名访问 m_Age: " << Person::m_Age << endl;
// cout << "other = " << Person::m_other << endl; // 私有权限在类外无法访问
// 3、静态成员函数调用
p1.func();
cout << "通过类名访问 m_Age: " << Person::m_Age << endl; // 100
p2.myFunc();
cout << "通过类名访问 m_Age: " << Person::m_Age << endl; // 200
Person::func();
cout << "通过类名访问 m_Age: " << Person::m_Age << endl; // 100
// Person::func2(); // 私有权限在类外无法访问
}
int main() {
test();
return 0;
}
// const 静态数据成员:最好在类内部初始化
class Person {
public:
//static const int mShare = 10;
const static int mShare = 10; // 只读区,不可修改
};
int main() {
cout << Person::mShare << endl; // 10
// Person::mShare = 20; // 不可修改
return 0;
}
5、常对象和常成员
- 常成员函数:类型说明符 函数名(参数表) const
- 常成员函数可以使用类中的所有成员变量,但
不能修改成员变量的值
,当成员变量类型符前用mutable
修饰时例外 - 构造函数和析构函数不能声明为
const
- 需要强调的是,必须在成员函数的声明和定义处同时加上 const 关键字
- 常成员函数可以使用类中的所有成员变量,但
- 常成员变量:
- 对不应该被修改的数据成员声明为
const
,可使其受到强制保护,初始化方式与一般数据成员不同,只能通过 构造函数的初始化列表 来进行初始化
- 对不应该被修改的数据成员声明为
- 常对象:
- 只能访问
const
修饰的成员函数 - 可访问
public
修饰的成员变量,但不能修改;除非成员变量用mutable
修饰,常对象可对其进行修改
- 只能访问
#include<iostream>
using namespace std;
class Person {
public:
Person() {
this->mAge = 0;
this->mID = 0;
}
// 在函数括号后面加上 const, 修饰成员变量不可修改,除了 mutable 变量
void SomeOperate() const {
// this->mAge = 200; // mAge 不可修改
this->mID = 10;
}
void ShowPerson() {
this->mAge = 1000;
cout << "ID:" << mID << " mAge:" << mAge << endl;
}
public:
int mAge;
mutable int mID;
};
int main() {
const Person p;
// p.mAge = 100; // 不可修改
p.mID = 1001; // 但是可以修改 mutable 修饰的成员变量
cout << "ID:" << p.mID << " mAge:" << p.mAge << endl; // ID:1001 mAge:0
// p.ShowPerson(); // 只能访问 const 修饰的函数
p.SomeOperate();
cout << "ID:" << p.mID << " mAge:" << p.mAge << endl; // ID:10 mAge:0
return 0;
}
6、友元函数
- 是一种特权函数,C++ 允许这个特权函数访问私有成员,但它不是类的成员,不带 this 指针
全局函数
(非成员函数)做友元函数:全局函数写到类中做声明,并且声明前面加上friend
关键字其它类中的成员函数
做友元函数:在类中做声明friend void goodGay::visit(); // visit 为其它类中的成员函数
其它整个类
做友元类:在类中做声明friend class ClassName
,友元关系是不能被继承的、单向的、不可传递的;一般不建议把整个类声明为友元类,而只将某些成员函数声明为友元函数,这样更安全一些
#include<iostream>
#include <string>
using namespace std;
class Building {
// 友元函数声明:让全局的好基友函数变为我的好朋友
// 在友元函数中不能直接访问类的成员,必须要借助对象
friend void goodGay(Building &building);
public:
Building() {
this->m_SittingRoom = "客厅";
this->m_BedRoom = "卧室";
}
public:
string m_SittingRoom; // 客厅
private:
string m_BedRoom; // 卧室
};
// 全局函数:好基友,全局范围内的非成员函数,它不属于任何类
void goodGay(Building &building) {
cout << "好基友正在访问: " << building.m_SittingRoom << endl;
cout << "好基友正在访问: " << building.m_BedRoom << endl;
}
// 友元函数 目的访问类中的私有成员属性
void test01() {
Building b;
goodGay(b); // 调用友元函数
}
int main() {
test01();
return 0;
}
三、继承和派生
1、继承的基本概念
- 定义:
- 通过继承机制,可以利用现有的类(吸收其属性和行为(方法),并对其进行覆盖和改写)来定义新类
- 新类不仅拥有新定义的成员(变量或函数),还同时拥有被继承类的成员,实现了
代码复用
- 基类(父类)和派生类(子类):
- 称已存在的用来派生新类的类为基类,又称父类
- 称由已存在的类派生出的新类为派生类,又称子类
- 派生类(子类)的定义:
// 单继承:从一个基类派生的继承
Class 派生类名: 继承方式 基类名 // 继承方式分为:public、protected、private
{
// 派生类新增的数据成员和成员函数
}
// 多继承:从多个基类派生的继承,当两个父类有重名的成员时,容易引发二义性(可通过作用域解决)
Class 派生类名: 继承方式1 基类名1, 继承方式2 基类名2
{
// 派生类新增的数据成员和成员函数
}
// 单继承示例
class Parent
{
public:
void Test()
{
printf("Parent ... \n");
}
public:
int a;
};
// 子类可以重写全部或者部分父类继承而来的函数
class Child : public Parent
{
public:
void Test()
{
Parent::Test(); // 显示地调用父类的函数
printf("Child ... \n"); // 对父类的 Test() 函数进行部分重写
}
public:
int b;
};
2、派生类的访问控制
- 派生类拥有基类中全部成员变量和成员方法(
除了构造和析构
),但是在派生类中,继承的成员并不一定能直接访问,不同的继承方式会导致不同的访问权限(编译器做了访问限制
),派生类的大小为:sizeof(Son) = sizeof(parent)+sizeof(Son_other)
- 无论哪一种继承方式,派生类都不可以访问基类的私有成员:继承方式中的
public、protected、private
是用来指明基类成员在派生类中的最高访问权限
的- 当以
public
方式继承时,派生类对基类成员的访问权限保持不变(私有成员不可访问) - 当以
protected
方式继承时,派生类对基类成员的访问权限全部降低为 protected(私有成员不可访问) - 当以
private
方式继承时,派生类对基类成员的访问权限全部降低为 private(私有成员不可访问) - 在派生类中访问基类
private
成员的唯一方法就是借助基类的非 private 成员函数
,如果基类没有非 private 成员函数,那么该成员在派生类中将无法访问
- 当以
3、继承中的构造和析构
- 创建和销毁一个子类对象时父类和子类构造函数与析构函数的调用顺序:
- 子类对象构造时,先调用父类的构造函数,再调用子类的构造函数
- 子类对象析构时,先调用子类的析构函数,再调用父类的构造函数
- 虚函数:可以由派生类重写其功能的函数,要将函数标记为
virtual
(虚拟的),只需要在其声明的开头使用virtual
关键字,virtual return_type func_name() {}
- 纯虚函数:当一个类包含一个或多个纯虚函数时(
virtual return_type func_name() = 0;
),它就变成了一个抽象类,不能直接实例化,只能被继承(这样做是由于基类存在的目的纯粹是为了提供共享的属性和方法) - 当一个类被继承时,应该将 父类的析构函数声明为
virtual
,否则在销毁子类对象时父类和子类的析构函数顺序将发生错乱 - 如果这个类在设计的时候,已经明确它不会被继承,则不需要声明为
virtual
class Parent
{
public:
Parent()
{
printf("Parent: 创建 \n");
a = b = 0;
}
Parent(int a, int b)
{
printf("Parent: 创建, 参数:a=%d, b=%d \n", a, b);
this->a = a;
this->b = b;
}
virtual ~Parent()
{
printf("Parent: 销毁 \n");
}
public:
// 表明这是一个将被重写的虚函数
virtual void PrintMessage()
{
printf("Hello ");
}
public:
int a, b;
};
class Child : public Parent
{
public:
// 当父类构造函数有参数时,需要在子类初始化列表(参数列表)中显示调用父类构造函数
Child() : Parent(1, 1)
{
printf("Child: 创建 \n");
}
~Child()
{
printf("Child: 销毁 \n");
}
public:
// override 标识符表明对基类函数进行重写
void PrintMessage() override
{
Parent::PrintMessage();
printf("World!\n");
}
};
#include<iostream>
using namespace std;
class Circle {
public:
int x; // 类的数据成员:不能在声明时进行初始化,只能通过成员函数来实现(一般用构造函数来实现)
int y;
int radius;
string name;
public:
// 默认构造函数 : x(x0), y(y0), radius(r0), name(name0)
Circle(int x0, int y0, int r0, string &name0) {
x = x0;
y = y0;
radius = r0;
name = name0;
printf("Parent: 默认构造函数 \n");
printf("x y radius name is %d %d %d %s\n", x, y, radius, name.c_str());
}
// 析构函数
~Circle() {
printf("Parent: 析构函数调用 \n");
}
};
class Child_Circle : public Circle {
public:
int radius1;
public:
// 当父类构造函数有参数时,需要在子类初始化列表(参数列表)中显示调用父类构造函数
// 列表方式,Child_Circle(int x1, int y1, int r1, string &name1, int r2): Circle(x1, y1, r1, name1), radius1(r2)
Child_Circle(int x1, int y1, int r1, string &name1, int r2) : Circle(x1, y1, r1, name1) {
radius1 = r2;
printf("x y radius radius2 name %d %d %d %d %s\n", x, y, radius, radius1, name.c_str());
printf("Child: 创建 \n");
}
~Child_Circle() {
printf("Child: 销毁 \n");
}
};
int main() {
string name_ = "man";
Child_Circle c(5, 8, 10, name_, 12); // 默认构造函数:实参覆盖默认初始化参数
return 0;
}
4、继承中同名成员的处理方法
- 如果子类与父类的
成员变量或函数
名称相同,子类会把父类的所有的同名成员变量或函数都隐藏
,想调用父类的方法,必须加上 .父类名 和 作用域运算符::
- 只有
一个作用域内的同名函数
才具有重载关系,不同作用域内的同名函数是会造成遮蔽,使得外层函数无效。派生类和基类拥有不同的作用域,所以它们的同名函数不具有重载关系
#include<iostream>
using namespace std;
class Base {
public:
Base() {
m_A = 100;
}
void func() {
cout << "Base func 调用" << endl;
}
void func(int a) {
cout << "Base func (int a) 调用" << endl;
}
int m_A;
};
class Son : public Base {
public:
Son() {
m_A = 200;
}
// 重名函数:不构成重载
void func() {
cout << "Son func 调用" << endl;
}
int m_A; // 重名变量
};
void test() {
Son s1;
cout << s1.m_A << endl; // 200
cout << s1.Base::m_A << endl; // 100, 通过作用有运算符调用父类中的 m_A
s1.func(); // Base func 调用
s1.Base::func(10); // Base func (int a) 调用
}
int main() {
test();
return 0;
}
5、虚拟继承
菱形问题: 一个类从共享一个公共基类的两个基类继承,为了解决多继承时的命名冲突和冗余数据问题,C++ 提出了
虚继承
(在继承方式前面加上virtual
关键字),使得在派生类中只保留一份公共基类
的成员
#include<iostream>
using namespace std;
class BigBase {
public:
BigBase() { mParam = 9; }
void func() { cout << "BigBase::func" << endl; }
public:
int mParam;
};
#if 0 // 虚继承
class Base1 : virtual public BigBase{};
class Base2 : virtual public BigBase{};
#else // 普通继承
class Base1 : public BigBase {};
class Base2 : public BigBase {};
#endif
class Derived : public Base1, public Base2 {};
int main() {
Derived derived;
// 1、多继承存在二义性,可通过作用域解决
// derived.func();
// cout << derived.mParam << endl;
cout << "derived.Base1::mParam:" << derived.Base1::mParam << endl; // 9
cout << "derived.Base2::mParam:" << derived.Base2::mParam << endl; // 9
// 2、重复继承,从 Base1 和 Base2 继承了两次 BigBase 的 mParam,造成空间浪费,可通过虚拟继承解决
cout << "Derived size:" << sizeof(Derived) << endl; // 8
return 0;
}
四、多态性
1、多态的基本概念
- 定义:
父类的引用或指针指向子类对象
,一般使用指针多一些,引用不可以改变其指向- 所谓多态性是指同一个操作作用于不同的对象会产生不同的响应
- 在基类中定义的属性和方法被子类继承后,可以具有不同的 数据类型或者表现行为(方法) 等特性
- 可以对一个类不同的实例对象调用相同的方法(继承重载),产生不同的结果
- 作用:
- 提供接口与具体实现之间的另一层隔离,从而将
what
和how
分离开来 - 多态性改善了代码的可读性和组织性,同时也使创建的程序具有
可扩展性
- 面向对象程序设计一个基本原则:
开闭原则(对修改关闭,对扩展开放)
- 提供接口与具体实现之间的另一层隔离,从而将
- 分类:静态多态(编译时多态)和动态多态(运行时多态)
- 静态多态包括
函数重载和运算符重载
,在编译阶段就可以确定函数的调用地址,并产生代码 - 动态多态通过
派生类和虚函数(基类中)
来实现,函数的调用地址不能在编译期间确定,而需要在运行时才能决定
- 静态多态包括
2、静态多态
- 函数重载:赋给同一函数名多个含义
- 运算符重载:
返回值类型 operator 运算符名称 (形参表列){ } // 可以将 operator 运算符名称这一部分看做函数名
- 使得一个自定义的类型(stuct/class)可以像基本类型一样支持加减乘除等多种操作
- 而且C++的设计者是理想主义者,其终极理想就是让自定义类型和基本类型用起来完全一样
- 重载操作符在形式上类似于成员函数,但不能把它等同于函数,因为有的操作符形式上比较古怪,有以下几点注意事项:
- 名称:
operator 要重载的操作符名称; eg: operator +
- 返回值:类型不变,总是为该对象的类型
- 参数:基本上也不变
- 受
public/protected/private
的限制
- 名称:
- 当执行
c3 = c1 + c2;
语句时,编译器检测到 + 号左边(+号具有左结合性,所以先检测左边)是一个对象,就会调用成员函数operator+()
,也就是转换为下面的形式:c3 = c1.operator+(c2);
- 能够重载的运算符包括:
+ - * / % ^ & | ~ ! = < > += -= *= /= %= ^= &= |= << >> <<= >>= == != <= >= && || ++ -- , ->* -> () [] new new[] delete delete[]
- 运算符重载的实质是将运算符重载为一个函数,使用运算符的表达式就被解释为对重载函数的调用
- 运算符可以重载为全局函数:此时函数的参数个数就是运算符的操作数个数,运算符的操作数就成为函数的实参
- 运算符也可以重载为成员函数:此时函数的参数个数就是运算符的操作数个数减一,运算符的操作数有一个成为函数作用的对象,其余的成为函数的实参
3、动态多态
动态多态的成立条件:
- 有继承且子类重写父类虚函数
- 重写函数:返回值,函数名字,函数参数,必须和父类完全一致(析构函数除外)
- 子类重写函数后面可以加上
override
关键字- 父类指针或引用指向子类对象,通过父类指针或引用来操作子类对象
Tree* p = new AppleTree();
,左侧为Tree*
,右侧为AppleTree*
,p 的类型是Tree*
,它指向的对象是AppleTree*
(如果发生了继承的关系,编译器允许进行类型转换)- 从普通逻辑来讲:苹果树是一种树,因而可以把
AppleTree*
视为一种Tree*
- 从语法本质上讲:子类对象的前半部分就是父类,因而可以将父类的引用或指针指向子类对象
a、virtual 和 override 关键字
-
当父类的成员函数需要子类重写,那么在父类应该将其 声明 为
virtual
(子类相应成员函数自动地就是virtual
)- 在基类中声明虚函数并不会强制要求子类一定要重写这个函数。在 子类中可以选择性地重写虚函数,如果子类不重写这个虚函数,它仍然可以继承基类中的虚函数实现。因此,即使子类不重写虚函数,通常也会将虚函数声明为虚函数以便于子类进行重写
- 即使子类不重写虚函数,基类中声明的虚函数仍然需要使用
virtual
关键字标识,以确保在继承关系中能够 正确处理多态性
-
当一个类被继承时,应该将 父类的析构函数声明为
virtual
,否则在销毁子类对象时父类和子类的析构函数顺序将发生错乱 -
virtual
的作用:根据对象的实际类型,调用相应类型的函数 -
多态原理解析:
- 当父类中有了虚函数后,内部结构就发生了改变,内部多了一个虚函数表指针
vfprt(virtual function pointer)
,指向vftable
虚函数表 - 创建对象调用构造函数时,编译器会将虚函数表指针指向自己的虚函数表,如果发生了重写,会替换掉虚函数表中的原有的
speak
,改为&Cat::speak
- 当父类中有了虚函数后,内部结构就发生了改变,内部多了一个虚函数表指针
#include<iostream>
using namespace std;
class Animal {
public:
// 父类:加上 virtual 关键字变成虚函数(virtual 关键字让基类指针能够访问派生类的成员函数)
virtual void speak() {
cout << "动物在说话" << endl;
}
};
class Cat : public Animal {
public:
// 子类:加上 override 关键字重写父类虚函数
void speak() override {
cout << "小猫在说话" << endl;
}
};
int main() {
Animal *p1 = new Animal; // 父类指针指向父类对象
p1->speak(); // 动物在说话
delete p1;
// 基类中有了虚函数,基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量),指向派生类对象时就使用派生类的成员。
// 换句话说,基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,我们将这种现象称为多态。
// C++提供多态的目的是:可以通过基类指针对所有派生类(包括直接派生和间接派生)的成员变量和成员函数进行“全方位”的访问,尤其是成员函数。
// 如果没有多态,我们只能访问成员变量。
Animal *p2 = new Cat; // 父类指针指向子类对象
p2->speak(); // 小猫在说话
delete p2;
return 0;
}
// 成员函数实现与类声明分离式写法
#include <iostream>
// 声明基类
class BaseClass {
public:
void function1() {
std::cout << "BaseClass - function1" << std::endl;
}
virtual void function2(int value); // 声明虚函数
};
// 实现基类的成员函数
void BaseClass::function2(int value) {
std::cout << "BaseClass - function2, value: " << value << std::endl;
}
// 声明子类,继承自基类
class DerivedClass : public BaseClass {
public:
void function2(int value) override; // 重写虚函数
};
// 实现子类的成员函数
void DerivedClass::function2(int value) {
std::cout << "DerivedClass - function2 (overridden), value: " << value << std::endl;
}
int main() {
BaseClass baseObj;
baseObj.function1();
baseObj.function2(10);
DerivedClass derivedObj;
derivedObj.function1();
derivedObj.function2(20); // 重写了基类的成员函数
return 0;
}
BaseClass - function1
BaseClass - function2, value: 10
BaseClass - function1
DerivedClass - function2 (overridden), value: 20
b、 noexcept
关键字
noexcept
是 C++11 引入的一个关键字,用来指示一个函数不抛出任何异常
。它的主要作用是告知编译器该函数不会抛出异常,从而允许编译器进行优化,并在运行时提供更好的性能- 当你重载一个虚函数时,派生类中的重载函数的异常规范必须与基类中虚函数的异常规范一致。否则,会导致编译错误。这是因为 C++ 要确保虚函数调用时的异常行为一致,避免在运行时发生异常传播的问题
- 如果基类的虚函数没有
noexcept
,则派生类中的对应函数不能声明为noexcept
,因为如果派生类的函数抛出异常,而基类函数无法处理这种异常时,程序将不符合预期的行为 - 如果基类的虚函数标记为
noexcept
,那么派生类的重载函数必须也标记为noexcept
,否则也会出现编译错误
- 如果基类的虚函数没有
// 基类和派生类均有 noexcept 声明
class Base {
public:
virtual void foo() noexcept { // 基类的 foo() 声明为 noexcept
// do something
}
};
class Derived : public Base {
public:
void foo() noexcept override { // 派生类的 foo() 必须也声明为 noexcept
// do something else
}
};
// 基类和派生类均无 noexcept 声明
class Base {
public:
virtual void foo() { // 基类的 foo() 没有 noexcept
// do something
}
};
class Derived : public Base {
public:
void foo() noexcept override { // 错误!派生类不能声明为 noexcept
// do something else
}
};
c、(纯)虚函数和抽象类
- 虚函数:使用关键字
virtual
,如virtual void func() {};
- 纯虚函数:使用关键字
virtual
,并在其后面加上=0
,如virtual void func() = 0;
- 抽象类:在基类中至少有一个纯虚函数(包括纯虚析构函数),那么此基类就称为抽象类(或纯虚类)
- 抽象类
不可以实例化对象
,如果子类继承了抽象类, 必须重写抽象类中的纯虚函数
- 抽象类的析构函数应该声明为
virtual
,因为它是被设计用于继承的 - 主要用途:充当接口规范(凡是遵循此规范的类,都必须实现指定的函数接口,通常是一系列接口),抽象类通常是作为基类,让派生类去实现纯虚函数。派生类必须实现纯虚函数才能被实例化。
- 抽象类
d、(纯)虚析构函数
- 虚析构函数:
virtual ~类名() {}
,解决了通过**父类指针指向子类对象(发生多态时)**内存释放不干净的问题(只调用了基类的析构函数,未调用派生类的析构函数)- 将基类的析构函数声明为虚函数后,派生类的析构函数也会自动成为虚函数。这个时候编译器会忽略指针的类型,而根据指针的指向来选择,这样发生多态时就可以先调用派生类的析构函数再调用基类的析构函数了
- 纯虚析构函数:
virtual ~类名() = 0
,类内声明 ,类外实现,若出现了纯虚析构函数,这个类也算抽象类- 抽
class Base{
public:
Base();
virtual ~Base(); // 虚析构函数
protected:
char *str;
};
e、向上类型转换和向下类型转换
- 基类转派生类:向下类型转换,不安全的
- 派生类转基类:向上类型转换,安全
- 如果发生多态:总是安全的
五、内部类与命名空间
- 为什么需要内部类?
- 避免命名冲突
- 如果一个类只在模块内部使用 (使用protected/private 修饰符限制),则可以实现类名隐藏
- 内部类的定义与使用:
class AAA
{
public:
// 定义一个内部类
class Inner
{
public:
char name[64];
};
};
int main()
{
AAA::Inner a ; // 定义一个对象 a, 类名要使用全称AAA::Inner
strcpy(a.name, "AnXin");
printf("Name: %s \n", a.name);
return 0;
}
// 内部类可以自由访问外部类的 public/protected/private 成员,反之不可以。
- 命名空间(
namespace
):解决命名冲突的终极手段
namespace XXX
{
// 把类、函数和全局变量写在这个大括号里面
void Test()
{
printf("hello,world!\n");
}
class Object
{
public:
int id;
};
} // 注意:这里不需要分号
// 使用 namespace 中的名字,要加上名字前缀,eg: XXX::Test(); XXX::Object obj;
// 解除前缀限制(要确保不会造成名字冲突),使用 using 关键字,eg: using namespace XXX; 使用 XXX 里面的所有名字
- C++构造函数的三种写法
- 父类成员变量可以在
子类中使用函数进行更改;当外部实例对象调用过此函数后,以后实例对象就可以直接使用更改后的值了
(注意此父类成员变量还是要先在父类构造函数中进行初始化)
#include<iostream>
using namespace std;
class Circle {
public:
int x;
int y;
int radius; // 类的数据成员:不能在声明时进行初始化,只能通过成员函数来实现(一般用构造函数来实现)
char *name; // 可以在子类中使用函数进行更改;当外部实例对象调用过此函数后,以后实例对象就可以直接使用更改后的值了
public:
// 默认构造函数:不需要传参,已经有了默认参数
// x,y,radius,name 以初始化列表的形式赋值(其中 name 不需要外部传参进行改变,需要外部传入的在构造函数中设置参数)
// 也可以在构造函数内部进行赋值
Circle(int x0 = 5, int y0 = 0, int r0 = 1) : x(x0), y(y0), radius(r0), name(nullptr) {
// x = x0;
// y = y0;
// radius = r0;
// name = nullptr;
printf("默认构造函数 \n");
printf("x y radius is %d %d %d %s\n", x, y, radius, name);
}
// 析构函数
~Circle() {
printf("析构函数调用 \n");
}
};
class Child_Circle : public Circle {
public:
int radius2;
void *pvClsOut;
public:
// 当父类构造函数有参数时,需要在子类初始化列表(参数列表)中显示调用父类构造函数
// 子类中新增的成员变量可以在构造函数中设置参数从外部传入(radius2);
// 也可以在子类中使用函数进行更改,当外部实例对象调用过此函数后,以后实例对象就可以直接使用更改后的值了(pvClsOut)
Child_Circle(int x1, int y1, int r1, int r2) : Circle(x1, y1, r1), radius2(r2), pvClsOut(nullptr) {
printf("x y radius radius2 %d %d %d %d %s %p\n", x, y, radius, radius2, name, pvClsOut);
printf("Child: 创建 \n");
}
void print_value() {
name = "han";
pvClsOut = (char *) "man";
printf("x y radius radius2 %d %d %d %d %s %s\n", x, y, radius, radius2, name, pvClsOut);
}
void print_value2() {
printf("x y radius radius2 %d %d %d %d %s %s\n", x, y, radius, radius2, name, pvClsOut);
}
~Child_Circle() {
// delete[] pvClsOut;
printf("Child: 销毁 \n");
}
};
int main() {
// Circle a; // 默认构造函数
// Circle b(1, 1, 4); // 默认构造函数:实参覆盖默认初始化参数
printf("########################\n");
Child_Circle c(5, 8, 10, 12);
c.print_value();
c.print_value2();
return 0;
}
class VidaModel
{
public:
explicit VidaModel(ModelOpenParams ¶ms,int32_t pLogLevel,char *algname);
// ~VidaModel(); // 析构函数声明
int32_t PreProcess(VIDAImg *pDataIn, int32_t inNum);
int32_t PreProcess(VIDAImg *pDataIn, int32_t inNum,PPParam *pparam);
int32_t Forward();
int32_t PostProcess();
int32_t Release();
int32_t OpenModel();
int32_t SaveOut();
int32_t loadModel();
int32_t freeSrc(); // 释放手动申请的资源
int32_t callocSrc();
void ShowVersion();
public:
void *pModelHdl;
ModelOpenParams *pInParam;
uint8_t *pu8ModelBuf;
uint64_t u64ModelBufSize;
int32_t PrintLevel;
char *Algname;
VidaLOG *P;
// 模型推理的输入输出信息记录 (输入输出层个数、名字、内存大小、内存地址、形状)
// 可以在子类中使用函数进行更改;当外部实例对象调用过此函数后,以后实例对象就可以直接使用更改后的值了
TVIDAModelIOInfo *IOInfo;
};
//-------分类相关-------
class VidaCls : public VidaModel
{
public:
// 重写构造函数和后处理函数
VidaCls(ModelOpenParams ¶ms, int32_t s32ClsNum,int32_t pLogLevel,char *algname): VidaModel(params,pLogLevel,algname),ClsNum(s32ClsNum), ClsOut(nullptr){}
int32_t PostProcess();
int32_t callocSrc();
int32_t freeSrc();
int32_t ClsNum;
//private:
// 需要在 callocSrc 中开辟内存;PostProcess中进行赋值,用于输出推理后的结果;freeSrc 中释放相应内存
TVIDAClsResult *ClsOut;
};