C++的类、模板等相关知识点

** C++三大特性**
封装、继承、多态
封装就是把数据和代码组合在一起形成类,避免不确定访问和外界干扰。类可以设置访问限制,把公共的数据和方法用public访问,私有的private.
在类的内部(定义类的代码内部),⽆论成员被声明为 public、protected 还是 private,都是可以互相访问的,
没有访问权限的限制。
在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访
问 private、protected 属性的成员。
⽆论共有继承、私有和保护继承,私有成员不能被“派⽣类”访问,基类中的共有和保护成员能被“派⽣类”访问。对于私有和保护继承,基类中的所有成员不能被“派⽣类对象”访问。

继承:它可以使⽤现有类的所有功能,并在⽆需新编写原来的类的情况下对这些功能进⾏扩展。代码复用。
(理解虚拟继承和多重继承的问题)
private: 只能由该类中的函数、其友元函数访问,不能被任何其他访问,该类的对象也不能访问.
protected: 可以被该类中的函数、子类的函数、以及其友元函数访问,但不能被该类的对象访问
public: 可以被该类中的函数、子类的函数、其友元函数访问,也可以由该类的对象访问

C++通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。在类的内部(定义类的代码内部),无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制。在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员。

一般成员变量放在私有部分,隐藏起来不让外部看到,成员函数一般是公有的,因为你定义成保护或私有,类外是无法访问这个函数的,没什么意义。成员函数一般是给外人使用,操作成员变量。
保护权限和私有权限在继承时会有区别,比如父亲的车可以作为保护成员,可以让儿子也使用,而父亲的私房钱应作为私有成员,儿子不能使用。
继承方式: 公共继承、保护继承、私有继承
继承语法:class 子类 : 继承方式 父类 默认的继承方式(如果缺省,默认为private继承)
父类私有的部分不管是哪种继承方式,子类都无法访问(除非父类在public或protected中定义了访问私有内容的函数你才有可能访问到)
保护继承的特点是基类的所有公有成员和保护成员都作为派生类的保护成员,并且只能被它的派生类成员函数或友元函数访问,基类的私有成员仍然为私有的。

友元:类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。
友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。如果要声明函数为一个类的友元,需要在类定义中该函数原型前使用关键字 friend. 函数的定义在类外。

class Box
{
    
    
   double width;
public:
   double length;
   friend void printWidth( Box box );
   void setWidth( double wid );
};

子类对父类成员的访问权限跟如何继承没有任何关系,“子类可以访问父类的public和protected成员,不可以访问父类的private成员”——这句话对任何一种继承都是成立的。
(2)继承修饰符影响着谁可以知道“继承”这件事。public继承大家都知道,有点像“法定继承人”,因此,任何代码都可以把子类的引用(或指针)直接转换为父类。也因为这个原因,public继承常用来表达设计中所谓的“is-a”关系。private继承则有点像“私生子”,除了子类自己,没有人知道这层关系,也因此,除了子类自己的代码之外,没有其它人知道自己还有个父亲,于是也就没有其它人可以做相应的类型转换。为此,私有继承常用于表达非“is-a”的关系,这种情况下子类只是借用父类的某些实现细节。protected继承则有点特殊,外界同样不知道这层关系,但家族内部的子孙们可以知道,有点像“自家知道就行了,不许外扬”的意思,于是子孙们是可以做这种向上转型,其它代码则不可以。因为这种特殊性,protected继承在实际中用得很少。
(3)还需要补充一点,由于“继承关系”的可见性受到了影响,那么继承来的财产的可见性也必然受到影响。比如一个成员变量或成员函数,在父类中本来是public的,被某个子类protected继承之后,对子类来讲,这个成员就相当于protected成员了——继承是继承到了,但权限变了。具体的规则教材上都会讲的。

多态:即向不同对象发送同⼀消息,不同的对象在接收时会产⽣不同的⾏为(重载实现编译时多态,虚函数实现运⾏时多态)
主要使用虚函数,用于当父类对象指针指向不同子类对象,表现的是虚函数的不同的行为。
基类是⼀个抽象对象——⼈,那学⽣、运动员也是⼈,⽽使⽤这个抽象对象既可以表示学⽣、也可以表示运动员。
虚函数依赖虚函数表⼯作,表来保存虚函数地址,当我们⽤基类指针指向派⽣类时,虚表指针指向派⽣类的虚函数表
为什么需要基类的指针指向派生类的对象,不用派生类自己的指针指向自己的对象?
因为可以用基类指针指向其不同派生类对象,所以才能实现多态和虚函数更为重要的是,当我们想要用某个数据结构去存储不同对象时,比如我们想实现一个所有动物对象“走”得数组,遍历数组时向我们展示出不同动物对象走的情况,该怎么做呢?因为我们的数组只能实现同一类型数据存储,而我们又想展示出不同类型对象的“走”,所以很显然我们只需在数组中存储基类指针,然后把数组中的每个基类指针与相应的类对象进行动态绑定即可(书面来说就是接口重用,提高代码可重用性,维护性扩充性)

vector<Animal*> walk;
	walk.emplace_back(new Bird());
	walk.emplace_back(new Reptile());
	walk.emplace_back(new Human());
	for (const auto &c : walk)
		c->walk();

动态绑定是如何实现的?
就是问动态多态怎么实现?当编译器发现类中有虚函数时,会创建一张虚函数表,把虚函数的函数入口地址放到虚函数表中并且在对象中增加一个指针vptr,用于指向类的虚函数表。当派生类覆盖基类的虚函数时,会将虚函数表中对应的指针进行替换,从而调用派生类中覆盖后的虚函数,从而实现动态绑定。
实现的必要条件就是:必须要有继承,有虚函数,有基类指针指向派生类对象
虚函数表是针对类的,类的所有对象共享这个类的虚函数表,因为每个对象内部都保存一个指向该类虚函数表的指针vptr,每个对象的vptr的存放地址都不同,但都指向同一虚函数表。在gcc编译器的实现中虚函数表vtable存放在可执行文件的只读数据段.rodata中。

纯虚函数有什么作用?如何实现?
定义纯虚函数是为了实现一个接口,起到规范的作用,想要继承这个类就必须覆盖该函数。所有人用同一个接口。
实现方式是在虚函数声明的结尾加上= 0即可。 virtual void fun()=0;

C++禁止使用拷贝构造函数和赋值运算符方法
要么将拷贝构造函数和赋值运算符声明为private并不实现。或者在后面加=delete。 nocopyable
(为什么要禁止?因为很多时候会存在有指针对象成员,默认的拷贝是浅拷贝,会出现二次析构的问题;还有就是不希望出现多个对象)
原则:
1、构造函数不能为虚函数;
2、析构函数需要是虚函数;

析构函数为什么要是虚函数: 防止内存泄露
因为我们可以用基类的指针指向派生类对象,我们希望调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。但是,如果析构函数不被声明成虚函数,则编译器采用的绑定方式是静态绑定,在删除基类指针时,只会调用基类析构函数,而不调用派生类析构函数,这样就会导致基类指针指向的派生类对象析构不完全。

构造函数不能为虚函数:不能也没必要
不能:虚函数有一个指向虚函数表的指针,这个虚函数表存储在对象的内存空间中,而构造函数就是实例化对象的,对象都没有,哪来的虚函数表。虚函数的调用依赖于虚函数表,而指向虚函数表的指针vptr需要在构造函数中进行初始化,所以无法调用定义为虚函数的构造函数。
没必要:虚函数的作用在于通过父类的指针或者引用来调用它的时候可以变成调用子类的那个成员函数。而构造函数是在创建对象时自己主动调用的。*

内存泄漏的场景有哪些?
1、没有成对使用new/delete ,malloc/free, new []/delete [] 比如在构造函数new申请内存,但是析构函数没有释放内存。
2、基类析构函数不是虚函数,导致派生类析构时不调用。为了实现动态绑定,基类指针指向派生类对象,如果析构函数不是虚函数,那么在对象销毁时,就会调用基类的析构函数,只能销毁派生类对象中的部分数据。
3. 如果类中有从堆中动态分配的指针变量,则类中必须定义拷贝构造函数。为什么呢?因为默认的是位拷贝(浅拷贝)。导致两个对象指向同一个内存地址,两次释放就会出问题。
明确一点,就算是一个空类,编译器也会加上默认的构造函数和拷贝构造函数、析构函数、赋值运算符。

构造函数:实例化一个对象,数据成员初始化以及分配内存的作用。特点是名字和类名一样,没有返回值。
构造函数可以被重载,有多个,带不同参数。析构函数一般为虚函数,只有一个不能重载。

拷贝构造函数是C++独有的,它是一种特殊的构造函数,用基于同一类的一个对象构造和初始化另一个对象。
A a;
A b(a);
A b=a; 都是拷贝构造函数来创建对象b
强调:这里b对象是不存在的,是用a 对象来构造和初始化b的!!
在C++中,3种对象需要复制,此时拷贝构造函数会被调用
1 一个对象以值传递的方式传入函数体
2 一个对象以值传递的方式从函数返回
3 一个对象需要通过另一个对象进行初始化

注意点:系统提供的默认拷贝构造函数工作方式是内存拷贝,也就是浅拷贝。如果复制的对象中引用了一个外部内容(例如分配在堆上的数据、指针),那么在复制这个对象的时候,让新旧两个对象指向同一个外部内容,析构两次就会崩溃,所以需要重载拷贝构造函数,完成深拷贝。A (const A&other)

当一个类的对象向该类的另一个对象赋值时,就会用到该类的赋值函数。
A a;
A b;
b=a;
强调:这里a,b对象是已经存在的,是用a 对象来赋值给b的!!
A& operator = (const A& other)

拷贝和赋值的区别:
拷贝是一个对象来初始化一块内存,这块内存就是新对象的内存区域;赋值是一个对象已经初始化了,再用另一个对象赋值。

如果不想写拷贝构造函数和赋值函数,又不允许别人使用编译器生成的缺省函数,最简单的办法是将拷贝构造函数和赋值函数声明为私有函数,不用编写代码。

虚拟继承和虚函数完全不一样。
虚函数不说了。
虚继承是解决C++多重继承问题的一种手段,从不同途径继承来的同一基类,会在子类中存在多份拷贝。这将存在两个问题:其一,浪费存储空间;第二,存在二义性问题,通常可以将派生类对象的地址赋值给基类对象,实现的具体方式是,将基类指针指向继承类(继承类有基类的拷贝)中的基类对象的地址,但是多重继承可能存在一个基类的多份拷贝,这就出现了二义性,不知道变量是从哪个基类继承的。

原理:
一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。
实际上,vbptr指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。
在这里我们可以对比虚函数的实现原理:他们有相似之处,都利用了虚指针(均占用类的存储空间)和虚表(均不占用类的存储空间)。
虚基类依旧存在继承类中,只占用存储空间;虚函数不占用存储空间。
虚基类表存储的是虚基类相对直接继承类的偏移;而虚函数表存储的是虚函数地址。

组合和继承的比较(组合优于继承?)
设计模式中,rust中都体现了组合优于继承的思想
继承是一种is-a的关系,父类通过继承子类的方法和变量,实现代码重用的目的,这是最大的优点。继承的相关知识点有公共继承,私有继承保护继承区别,多重继承,虚拟继承的概念等等。继承的缺点如下:
1、大千世界,关系很复杂,继承关系也变得复杂,比如想飞行就要继承鸟,想下海就要继承鱼。这就会产生多重继承,会产生二义性。可以用虚基类解决。而且java里面不支持多个基类,所以从这里就可以看出继承的其中一个缺点:无法通过继承的方式,重用多个类中的代码。
2、继承破坏了封装性。因为继承可以访问基类的成员,自然就破坏了封装性,除非你把成员变量设置为私有的,这样子类就不能直接访问。更重要的是,子类必须无条件接收基类的方法,不管是不是完全适用。所以不该用的方法就暴露被污染了。
3、继承无法实现动态继承。继承是编译期就决定下来的,无法在运行时改变。组合加反射就可以。
4、继承是紧密耦合的,如果父类接口改变, 子类必须修改,特别是当不同组人员维护开发,更麻烦。
如何解决这些问题?
《劝学》”假舆马者,非利足也,而致千里;假舟楫者,非能水也,而绝江河。君子生非异也,善假于物也
君子其实没什么太多特别的地方,只不过善于利用工具而已。这就是所谓的”has-a”。拥有什么,或者使用什么。组合人想上天怎么办呢?可以利用飞机上天。人想下海怎么办呢,可以利用轮船下海。并不要求人要长出翅膀,人要长出鱼尾。把一些特征和行为抽取出来,形成工具类。然后通过聚合/组合成为当前类的属性。再调用其中的属性和行为达到代码重用的目的。

什么时候用继承:父类的方法子类完全适用,子类只需要用一个父类的方法,方法不会在运行时根据需求改变。

拷贝构造函数注意点 A(const A &a);
拷贝构造函数必须是当前类的引用
如果拷贝构造函数的参数不是当前类的引用,而是当前类的对象,那么在调用拷贝构造函数时,会将另外一个对象直接传递给形参,这本身就是一次拷贝,会再次调用拷贝构造函数,然后又将一个对象直接传递给了形参,将继续调用拷贝构造函数……这个过程会一直持续下去,没有尽头**,陷入死循环**。
只有当参数是当前类的引用时,才不会导致再次调用拷贝构造函数,这不仅是逻辑上的要求,也是 C++ 语法的要求。
拷贝构造函数是const 引用
拷贝构造函数的目的是用其它对象的数据来初始化当前对象,并没有期望更改其它对象的数据,添加 const 限制后,这个含义更加明确了。
另外一个原因是,添加 const 限制后,可以将 const 对象和非 const 对象传递给形参了,因为非 const 类型可以转换为 const 类型。如果没有 const 限制,就不能将 const 对象传递给形参,因为 const 类型不能转换为非 const 类型,这就意味着,不能使用 const 对象来初始化当前对象了。
没有返回类型

Student::Student(const Student &stu){
    
    
    this->m_name = stu.m_name;
    this->m_age = stu.m_age;
    this->m_score = stu.m_score;
    cout<<"Copy constructor was called."<<endl;
}
**//重载=运算符
Student & Student::operator=(const Student &stu){
    
    
    this->m_name = stu.m_name;
    this->m_age = stu.m_age;
    this->m_score = stu.m_score;
    cout<<"operator=() was called."<<endl;
   
    return *this;
}**

重载赋值运算符是返回引用。 赋值运算符写的时候还要注意就是要先删除之前动态申请的空间,重新申请。还要判断是不是本身,是本身直接返回*this 。

拷贝构造函数的应用场景:
将其它对象作为实参 A a(b);
Student stu4 = stu1; //在创建对象的同时赋值
stu5=display(stu5); //函数形参,返回值调用
(返回值优化(Return value optimization,缩写为RVO)是C++的一项编译优化技术。它最大的好处是在于: 可以省略函数返回过程中复制构造函数的多余调用,解决 “C++ 中长久以来为人们所诟病的临时对象的效率问题”。)
Student stu5; //调用普通构造函数Student()
stu5 = stu1; //调用operator=()

内存分配方式有几种?
1、栈分配,比如函数的局部变量,函数结束自动销毁,分配效率很高,但是栈容量有限。
2、堆分配(new,malloc)释放需要程序员自己控制;
3.自由存储区分配,和堆比较像,但不是一个空间,比定位new运算符就是指定一个地址分配,这个地址属于自由存储区。
4、常量存储区,无法修改;
5.全局、静态存储区,初始化和未初始化BSS,C++已经不区分初始化和未初始化了,自动初始化为0。

如何构造一个类,使得只能在堆上或只能在栈上分配内存?
//A a;//栈上创建
A* p = new A;//堆上创建
在C++中,创建类的对象有两种方法,一种是静态建立,A a; 另一种是动态建立,调用new 操作符。
只能在堆上分配内存:将析构函数声明为private;
编译器管理了对象的整个生命周期,编译器为对象分配空间的时候,只要是非静态的函数都会检查,包括析构函数,但是此时析构函数不可访问,编译器无法调用类的析构函数来释放内存,那么编译器将无法在栈上为对象分配内存。

只能在栈上生成对象:将new和delete重载为private。

private:
    void* operator new(size_t)
    {
    
    };
    void operator delete(void*)
    {
    
    };

如何让类只能创建一个对象?
单例模式。 构造函数,拷贝赋值都私有,创建一个静态的成员,静态的函数返回这个成员。

class singleton
{
    
    
public:
 static singleton* getInstance()
 {
    
    
  return &_single;
 }
private:
 //构造函数私有
 singleton(){
    
    };
 //防拷贝
 singleton(const singleton& s) = delete;
 singleton& operator=(const singleton& s) = delete;
 static singleton _single;
};

//静态成员的初始化
singleton singleton::_single;
// 在程序入口之前就完成单例对象的初始化

结构体内存对齐规则
一、结构体对齐规则首先要看有没有用**#pragma pack宏声明**,这个宏可以改变对齐规则,有宏定义的情况下结构体的自身宽度就是宏上规定的数值大小,所有内存都按照这个宽度去布局(这样说其实不太严谨,后面会提到),#pragma pack 参数只能是 ‘1’, ‘2’, ‘4’, ‘8’, or ‘16’。不过如果最大的类型都没有pack定义的大,就根据最大的类型优化。
二、在没有#pragma pack这个宏的声明下,遵循下面三个原则:
1、第一个成员的首地址为0.
2、每个成员的首地址是自身大小的整数倍
3、结构体的总大小,为其成员中所含最大类型的整数倍。

union就是都是顶端对齐,大小是最大成员的大小。

26 类模板和模板特化
类模板
template
class A {}; // 类模板是能接受任意类型,A后面不需要(不能)任何处理
模板偏特化(局部特化) 可以接受任意指针类型
// A
template
class A<T *> {}; // 类模板A的偏特化版本,在A后指出特化的范围

指定接受int类型
template<>
class A {} // 类模板A的全特化版本(已经是类模板的一个实例了),在A后直接指出明确类型int

全特化优先级最高。

27 强制类型转换的几种方法
static_cast:没有运⾏时类型检查来保证转换的安全性进⾏上⾏转换(基类指针=派生类指针)是安全的
进⾏下⾏转换(把基类的指针或引⽤转换为派⽣类表示),由于没有动态类型检查,所以是不安全的。
使⽤:

  1. ⽤于基本数据类型之间的转换,如把int转换成char。
  2. 把任何类型的表达式转换成void类型

dynamic_cast:主要用于“安全地向下转型”。在进⾏下⾏转换时,dynamic_cast具有类型检查(信息在虚函数中)的功能,⽐static_cast更安全。
转换后必须是类的指针、引⽤或者void*,基类要有虚函数,因为dynamic_cast需要从类的虚函数表表中获得类类型信息。
dynamic本身只能⽤于存在虚函数的⽗⼦关系的强制类型转换;对于指针,转换失败则返回nullptr,对于引⽤,转
换失败会抛出异常。

reinterpret_cast
可以将整型转换为指针,也可以把指针转换为数组;可以在不同类型的指针间进行任意转换,其实就是重新解释了内存取值,指针是一样的,但是指针的内容怎么解释(取几个字节),这个转换了一下。错误的使用reinterpret_cast很容易导致程序的不安全,只有将转换后的类型值转换回到其原始类型,这样才是正确使用reinterpret_cast方式。它的使用价值:用来辅助哈希函数,把void当成Int,对整数的操作显然要对地址操作更方便。
const_cast 常量指针转换为⾮常量指针,并且仍然指向原来的对象。常量引⽤被转换为⾮常量引⽤,并且仍然指向原来的对
象。去掉类型的const或volatile属性。
去掉const属性:const_cast<int
> (&num),常用,因为不能把一个const变量直接赋给一个非const变量,必须要转换。
但是,如果把常量A的指针改成非常量指针,然后赋给一个非常量B,修改B值,常量A的值并没有变。但是它们地址是相同的。很奇怪。如果我们不想修改const变量的值,那我们又为什么要去const呢?原因是,我们可能调用了一个参数不是const的函数,而我们要传进去的实际参数确实const的,但是我们知道这个函数是不会对参数做修改的。于是我们就需要使用const_cast去除const限定,以便函数能够接受这个实际参数。

猜你喜欢

转载自blog.csdn.net/weixin_53344209/article/details/130480972