一、类定义关键字
1.class和struct
- 区别
- 访问和继承属性:
struct 默认是公有
,class默认是私有
- 具体作用:
struct关键字
兼容C中的结构体声明
的作用,class
可用于定义模板参数
- 访问和继承属性:
2.类的访问权限
- 访问权限的区别
- 修饰变量和函数:
- public:在类的内部外部都可以访问
- protected:在
本类
和派生类
中可以访问 - private:
只能在本类和友元类
中访问
- 权限继承:
- public继承:基类中各成员属性保持不变
- protected继承:除private,基类中各成员属性均变为protected
- private继承:除private,基类中的各成员属性全变成private
- 派生类对象的访问权限
- 任何继承中,基类的private成员都会被隐藏,派生类不能访问。但是派生类对象的内存中有基类的private成员变量,可以通过指针偏移的方式进行访问。
- 派生类实例化的对象只能访问权限为public的成员
- 修饰变量和函数:
- 派生类对基类的访问
- 内部访问:由派生类中新增的成员函数对从基类继承来的成员的访问
- 外部访问:在派生类外部,通过派生类的对象对从基类继承来的成员的访问
- 除了public继承的public其他外部访问均不可
- 单一分配方式的实现
- 只静态分配:
将new和delete运算符重载为private属性
。因为只能静态分配意味着无法使用new在堆上分配空间,private属性的成员无法被外部调用,因而无法通过 new动态创建对象class test{ private:// 重载new和delete运算符,并设置为private属性 void operator new()(size_t t){} void operator delete(void *p){} public: test(){} ~test(){} };
- 只动态分配:
在类中将析构函数设置为private,并额外自定义destory析构函数
,编译器分配栈对象时,会检查类的析构函数的可访问性,如果析构函数在类外无法访问,则编译器会拒绝为类对象在栈中分配空间。class A { public: A(){} void destory(){delete this;}// 额外定义析构函数释放内存 private: ~A(){}// 将析构函数定义为私有 };
- 只静态分配:
- 虚继承
- 原因:解决
菱形继承
(一个派生类同时继承了两个基类,而这两个基类又继承自同一个基类)导致的派生类中有两份相同的基类型结构
,从而出现数据冗余和不一致性等问题。 - 解决方式
// 最初基类 class Animal { public: int age; }; class Mammal : virtual public Animal { public: // ... }; class Bird : virtual public Animal { public: // ... }; // 最终继承类 class Platypus : public Mammal, public Bird { public: // ... };
- 原因:解决
3.友元类和友元函数
-
概述
- 定义:友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的私有成员和保护成员
- 特点:
- 单向性:如果类A是类B的友元,那么类B不一定是类A的友元。
- 破坏封装:友元类的访问突破了该类的封装
- 不可继承:友元关系不能被继承,友元关系只在类的作用域内有效
class A { public: friend class C; //这是友元类的声明 private: int data; }; class C { //友元类定义,为了访问类A中的成员 public: void set_show(int x, A &a) { a.data = x; cout<<a.data<<endl;} }; int main(void) { class A a; class C c; c.set_show(1, a); }
二、类的三大特性
封装
- 基本概念
- 定义:将
同类对象
的属性
和行为
抽象封装在一个类
中 - 高内聚低耦合:
隐藏实现细节
,不让类以外的程序直接访问或修改类内成员,只能提供的访问的公共接口
- 定义:将
继承
- 基本概念
- 通过派生类对基类进行
拓展
,从而实现代码复用
和层次化设计
- 实现方式
- 实现继承:派生类同时继承函数的接口和实现
- 接口继承:派生类只继承函数结构声明(名称)
- 可视继承:指子窗体(类)使用基窗体(类)的外观和实现代码的能力
- 通过派生类对基类进行
- 抽象类
- 定义:带有
纯虚函数
的类为抽象类 - 作用:为
派生类
定义一组必须实现或向下传递
的接口函数
- 其中若派生类是抽象类则可以向下传递,如果不是则必须实现。
- 继承
- 抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。
- 派生类要完全实现基类中所有的的纯虚函数,否则,派生类也变成了抽象类
class Shape { public: virtual void draw() = 0; // 纯虚函数,定义Shape类的接口 }; class Circle : public Shape { public: void draw() override { // 实现Shape类的接口 cout << "Drawing a circle." << endl; } }; class Square : public Shape { public: void draw() override { // 实现Shape类的接口 cout << "Drawing a square." << endl; } }; int main() { // Shape shape; // 抽象类对象不能直接构造 Shape* shape1 = new Circle(); // 通过派生类创建对象,实际创建的是Circle对象 Shape* shape2 = new Square(); // 通过派生类创建对象,实际创建的是Square对象 shape1->draw(); // Drawing a circle. shape2->draw(); // Drawing a square. return 0; }
- 定义:带有
- 多继承
- 优点:对象可以调用多个基类中的接口
- 缺点:如果派生类所继承的多个基类有相同的基类,而派生类对象需要调用这个祖先类的接口方法,就会容易出现二义性。
- 解决方法:加上全局符确定调用的具体类 / 使用虚拟继承。
多态
- 基本概念
- 相同基类的不同类对象调用同一基类函数会产生不同的行为
- 实现方式
重写override
:子类重定义
父类的虚函数,然后通过子类指针的向上转换
实现重载overload
:允许存在同名函数
而使用参数列表差异化
进行调用
- 虚函数相关基本概念
- 虚函数:声明为virtual类型的,基类非静态成员函数
- 虚基类:含有虚函数的基类
- 虚函数表:
每个虚基类拥有一张虚函数表
,虚函数表由类中的虚函数组成,该类所有对象都通过自己的虚表指针共享该虚函数表 - 虚基类的派生类:
继承虚基类的派生类也拥有自己的虚函数表,通过派生类的虚表指针进行索引
,虚派生类对象中虚表指针数量与继承的虚基类个数相等。在虚基类派生类的虚函数表中,未重写的虚函数是对应虚基类的虚函数地址,重写的虚函数是派生类的虚函数地址 - 纯虚函数:
virtual type fun() = 0;
含有纯虚函数得类是抽象类 虚函数表的大小在编译时确定
,位于只读数据段(.rodata),虚函数则位于代码段(.text)
- 多态
- 类型
- 静态多态:在
编译期
间实现,包括函数重载
和函数模板
- 动态多态:在
运行时
实现,包括虚函数
- 静态多态:在
- 类型
- 为什么调用普通函数比调用虚函数的效率高?
- 普通函数是静态联编的,在编译的时候确定函数地址,调用时直接call即可
- 虚函数是动态联编的,首先取到对象的首地址(this),然后再解引用取到虚函数表的首地址(vptr),再加上偏移量才能找到要调用的虚函数地址(vfunc),最后call调用。
- 虚函数相关问题
- 构造函数的不能是虚函数:构造函数执行完成才能完成对象的创建,而虚函数由对象的虚表指针的虚函数表索引,所以对象没有构建出来前无法调用虚构造函数。
- 不要在构造函数中调用虚函数的原因:因为父类对象会在子类之前进行构造,此时子类部分的数据成员还未初始化, 因此调用子类的虚函数是不安全的,故而C++不会进行动态联编。
- 不要在析构函数中调用虚函数的原因:析构函数调用时,先调用子类的析构函数,然后再调用基类的析构函数。所以调用基类的析构函数时,派生类对象的数据成员已经“销毁”,这个时再调用子类的虚函数已经没有意义了。
- 将虚函数声明为inline
多态调用不开内联展开
:当是指向派生类的指针(多态性)调用声明为inline的虚函数时,不会内联展开对象本身调用虚函数
:在函数不复杂的情况下会内联展开。
- 静态函数不能定义成虚函数的原因
- 虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,因为它是类的一个成员,并且vptr指向保存虚函数地址的vtable.对于静态成员函数,它没有this指针,所以无法访问vptr。这就是为何static函数不能为virtual,虚函数的调用关系:
this -> vptr -> vtable ->virtual function
- static成员函数不能被virtual修饰,static成员不属于任何对象或实例,所以加上virtual没有任何实际意义;静态成员函数没有this指针,虚函数的实现是为每一个对象分配一个vptr指针,而vptr是通过this指针调用的,所以不能为virtual;虚函数的调用关系,this->vptr->ctable->virtual function
- 虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,因为它是类的一个成员,并且vptr指向保存虚函数地址的vtable.对于静态成员函数,它没有this指针,所以无法访问vptr。这就是为何static函数不能为virtual,虚函数的调用关系:
- 子类赋值给父类,然后父类多态调用对应的子类
- 虚基类的析构函数必须声明为虚函数
- 原因:基类指针指向子类对象,delete基类指针,会调用基类析构函数,不会调用子类析构函数,造成内存泄露。
- 原理:虚析构函数会先调动子类的析构函数,再调动父类的析构函数。
- 子类指针赋值给基类指针
Base *base = new Derived();
- 访问:这个基类指针只能访问
基类的成员变量和成员函数
,但访问的基类虚函数是派生类重写的 - 原理:子类赋值给父类,只能获取到父类的数据成员,但虚表指针vptr仍然是子类的。
- 访问:这个基类指针只能访问
class Base{ public: virtual interfaceFun() = 0;// 基类定义纯虚的接口函数 virtual ~Base() = 0; // ***虚析构函数解决基类指针释放子类对象不干净,造成内存泄漏的问题 }; class Derive : public Base{ public: virtual interfaceFun() {// 重写基类虚函数 cout << derive; } ~Base(){// 自定义的delete delete attribute; }; private: int *attribute;// 派生类属性 }; // 主调函数中的调用 Base *base = new Derived();// 基类指针 Derive *d = (Derive *)&base;// 向下的类型转换必须显式的声明 base->interfaceFun();// 调用的派生类重写的虚函数
- 虚基类的析构函数必须声明为虚函数
三、模板
概述
- 类型参数化:将数据类型参数化,在类或函数的定义时不指明具体的数据类型,当调用时,编译器根据传入的实参自动推断数据类型。
- 模板定义:模板是一些类或函数的规则描述,可以通过类型参数实例化成具体的类或函数。
- 类型
- 函数模板
- 类模板
- 特点。
- 特例化(相同中允许要少量的不同):模板可以使得多种类型具有相同的规则,但是可能某些类型需要进行特殊化的处理
- 不提供隐式的参数类型转换,必须是严格的匹配
- 注意事项
- 如果在全局域中声明了与模板参数同名的变量,则该变量被隐藏
- 模板参数名不能被当作类模板定义中类成员的名字。
- 同一个模板参数名在模板参数表中只能出现一次
- 在不同的类模板声明或定义中,模板参数名可以被重复使用,但可在多个模板声明或定义之间重复使用
- 模板的定义和实现通常都放在同一个头文件中的原因
- 因为在编译时模板并不能生成真正的二进制代码,而是在编译调用模板类或函数的CPP文件时才会去找对应的模板声明和实现,在这种情况下编译器是不知道实现模板类或函数的CPP文件的存在,所以它只能找到模板类或函数的声明而找不到实现,而只好创建一个符号寄希望于链接程序找地址。但模板类或函数的实现并不能被编译成二进制代码,结果链接程序找不到地址只好报错了
函数模板
- 定义:将返回值类型、形参类型、局部变量类型均进行参数化而实现的一个抽象函数
- 原理
- 函数模板不能直接被调用,编译器会对函数模板进行两次编译:在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译。
- 示例
// 模板的创建 template<typename/class T>// 建议使用typename,class是为了兼容C++98以前的模板创建 T Add(const T& a, const T& b){ return a + b; } int a = 1; int b = 1; double b1 = 1; // 隐式实例化: 让编译器根据函数实参推演函数模板参数的实际类型 Add(a, b1);// 错误,参数类型不一致,编译器无法进行模板参数类型的推导 Add(a, b);// 通过,编译器会进行隐式的模板参数类型T推导为int // 显示实例化:在函数名后指定模板参数的实际类型 Add<double,int>(1.1, 2);
- 模板的特例化
- 必须为原函数模板的每个模板参数都提供实参,且使用关键字template后跟一个空尖括号对<>
- 特例化本质是实例化一个模板,在进行模板匹配的时候,系统会优先匹配特例化版本
// 模板的特例化 template<typename T> //模板函数 int compare(const T &v1,const T &v2) { if(v1 > v2) return -1; if(v2 > v1) return 1; return 0; } //模板特例化,满足针对字符串特定的比较,要提供所有实参,这里只有一个T template<> int compare(const char* const &v1,const char* const &v2) { return strcmp(p1,p2); }
类模板
-
定义:将类的成员变量和成员函数中的类型均进行参数化而实现的一个抽象类
-
示例
template<typename T1, typename T2> class B{ public: T1 a; T2 b; T1 func(T1 a, T2& b); }; B <int, string> b; //创建模板类B的一个对象 B<int, string> *b = new B<int, string>(1, 'a');// 使用对象指针的方式来实例化
-
特例化
- 可以只特例化类中的部分成员template<typename T> class Foo{ void Bar(); void Barst(T a)(); }; template<> void Foo<int>::Bar(){ //进行int类型的特例化处理 cout << "我是int型特例化" << endl; } Foo<string> fs; Foo<int> fi;//使用特例化 fs.Bar();//使用的是普通模板,即Foo<string>::Bar() fi.Bar();//特例化版本,执行Foo<int>::Bar() //Foo<string>::Bar()和Foo<int>::Bar()功能不同