目录
- C++类
C++类
类是用户定义的类型的定义,包括数据成员和成员函数。
默认为private.
访问权限
public、protected、private三个关键字来控制成员变量和成员函数的访问权限。他们分别表示为共有的、受保护的、私有的,被称为成员访问限定符。
访问 | public | protected | private |
---|---|---|---|
同类 | yes | yes | yes |
派生类 | yes | yes | no |
类外 | yes | no | no |
public: 可以被该类中的函数、子类的函数、其友元函数访问,也可以由该类的对象访问
protected: 可以被该类中的函数、子类的函数、以及其友元函数访问,但不能被该类的对象访问
private :只能由该类中的函数、其友元函数访问,不能被任何其他访问,该类的对象也不能访问。
对于私有继承,基类的共有和保护成员将成为派生类的私有成员,派生类只能通过基类接口访问私有数据和方法。
虚函数
1. 定义
虚函数是指基类中用virtual声明的成员函数.
当你用一个基类指针或引用指向一个派生类对象的时候,你调用一个虚函数,实际调用的是派生类的版本。
用virtual修饰的成员函数。
class A{
public:
virtual void f();
};
void A::f(){
cout << "A f" << endl;
}
class B:public A{
public:
void f();
};
void B::f(){
cout << "B f" << endl;
}
class C:public B{
public:
void f();
};
void C::f(){
cout << "C f" << endl;
}
int main()
{
A a;
a.f(); //A f
A *pa = new C();
pa->f(); //C f
return 0;
}
底层实现
实现原理: 虚函数表 + 虚表指针
编译器处理虚函数的方法是:为每个类对象都添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,称为虚表指针(vptr),这种数组称为虚函数表(virtual function table, vtbl)。
即每一个类使用一个虚函数表,每个类对象使用一个虚表指针。
this指针->vptr(4字节)->vtable ->virtual虚函数
C++虚函数的作用和实现原理_c++虚函数作用及底层原理-CSDN博客
2. 构造函数/析构函数
构造函数不能是虚函数。在构造函数中调用虚函数,应该实际执行的是父类的对应函数。
析构函数可以是虚函数。
3. 抽象类/纯虚函数
在虚函数的后面写上 =0 ,则这个函数就变成纯虚函数。
包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。
派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
这个纯虚函数的作用就是强迫我们重写虚函数,构成多态。
virtual 函数类型 函数名(形参列表) = 0;
常见问题
1. 虚函数不可以声明为 inline 吗
-
虚函数用于实现
运行时
的多态,或者称为晚绑定或动态绑定。而内联函数用于提高运行时的效率,原理是在
编译期间
对调用内联函数的地方的代码替换成函数代码。 -
在编译器期间并不知道需要调用的是父类还是子类的虚函数,所以不能够inline声明展开,所以编译器会忽略
2. 构造函数为什么不能为虚函数?
-
构造一个对象时,必须知道对象实际类型,但是虚函数是在运行期间确定实际类型的。
在构造一个对象时,由于对象还未构造成功,编译器无法知道对象的实际类型,是该类本身,还是派生类。
-
虚函数的执行依赖虚函数表,而虚函数表是在构造函数中进行初始化的,即初始化虚表指针,使得正确的指向虚函数表。而在构造对象期间,虚函数表还未初始化,将无法进行。
3. 析构函数为什么可以为虚函数?
-
析构函数定义为虚函数时:基类指针指向派生类,如果删除该指针,会调用该指针指向的派生类析构函数,而派生类的析构函数又会自动调用基类的析构函数。
将基类的析构函数声明为虚函数,当撤销基类对象的同时也撤销派生类的对象,这个过程是动态关联的。
-
将析构函数设置为虚函数是为了防止内存泄漏。
在继承体系中,当基类的指针或引用指向派生类,如果不是虚函数,只调用指针类型的析构函数。用基类delete,如果析构函数没有声明为虚函数,只能析构基类对象,派生类对象将无法析构。
C++中析构函数为虚函数_c++ 析构函数 虚函数-CSDN博客
4. 构造函数和析构函数可以调用虚函数吗?
- 在C++中,提倡不在构造函数和析构函数中调用虚函数。
- 构造函数和析构函数调用虚函数时都不使用动态联编,如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本。
- 因为父类对象会在子类之前进行构造,此时子类部分的数据还未初始化,因此调用子类的虚函数不安全,故而C++不会进行动态联编。
- 析构函数是用来销毁一个对象,在销毁一个对象时,先调用子类的析构函数,然后再调用基类的析构函数。所以在调用基类的析构函数时,派生类对象的数据成员已经销毁,这个时候再调用子类的虚函数没有任何意义。
5. 虚析构函数的作用,父类的析构函数是否要设置为虚函数?*
(1)C++中基类采用virtual虚析构函数是为了防止内存泄漏。具体的说,如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。
假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会出发动态绑定,因此指挥调用基类的析构函数,不会调用派生类的析构函数。那么此时派生类中申请的内存空间就不能释放,产生内存泄漏。
(2)纯虚析构函数一定得定义,因为每一个派生类析构函数会被编译器加以扩张,以静态调用的方式调用其每一个虚基类以及上一层基类的析构函数。
因此,缺乏任何一个基类析构函数的定义,就会导致连接失败。因此,最好不要把虚析构函数定义为纯虚函数。
6. 抽象基类为什么不能创建对象?*
(1)抽象类的定义
带有纯虚函数的类称为抽象类。
(2)作用
抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由 它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。所以 派生类实际上刻画了一组子类的操作接口的通用语义,这些语义也传给子类,子类可以 具体实现这些语义,也可以再将这些语义传给自己的子类。
(3)使用抽象类时注意: 抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没 有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。
如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可 以建立对象的具体的类。
抽象类是不能定义对象的。一个纯虚函数不需要(但是可以)被定义。
一、纯虚函数定义
纯虚函数是一种特殊的虚函数,它的一般格式如下:
class <类名> {
virtual <类型><函数名>(<参数表>)=0;
};
在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数, 它的实现留给该基类的派生类去做。这就是纯虚函数的作用。
纯虚函数可以让类先具有一个操作名称,而没有操作内容,让派生类在继承时再去 具体地给出定义。凡是含有纯虚函数的类叫做抽象类。这种类不能声明对象,只是作为 基类为派生类服务。除非在派生类中完全实现基类中所有的的纯虚函数,否则,派生类 也变成了抽象类,不能实例化对象。
二、纯虚函数引入原因
1、为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。
2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以 派生出老虎、孔 雀等子类,但动物本身生成对象明显不合常理。
为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;)。若要使派生类为非抽象类,则编译器要求在派生类中, 必须对纯虚函数予以重载以实现多态性。同时含有纯虚函数的类称为抽象类,它不能生 成对象。这样就很好地解决了上述两个问题。
例如,绘画程序中,shape 作为一个基类可以派生出圆形、矩形、正方形、梯形等, 如 果我要求面积总和的话,那么会可以使用一个 shape * 的数组,只要依次调用派生类 的 area()函数了。如果不用接口就没法定义成数组,因为既可以是 circle ,也可以是 square ,而且以后还可能加上 rectangle,等等.
三、相似概念
1、多态性
指相同对象收到不同消息或不同对象收到相同消息时产生不同的实现动作。
C++支持两 种多态性:编译时多态性,运行时多态性。
a.编译时多态性:通过重载函数实现
b.运行时多态性:通过虚函数实现。
2、虚函数
虚函数是在基类中被声明为 virtual,并在派生类中重新定义的成员函数,可实现 成员函数的动态重载。
3、抽象类
包含纯虚函数的类称为抽象类。由于抽象类包含了没有定义的纯虚函数,所以不能 定义抽象类的对象。
7.哪些函数不能是虚函数?
(1) 构造函数:构造函数还未初始化虚表;不知道调用哪个虚函数;
(2) 内联函数:内联函数在编译阶段进行函数体替换;
(3)静态函数:静态函数不属于对象属于类,没有this指针
(4)友元函数:友元函数不属于类的成员函数
(5)普通函数: 普通函数不属于类的成员函数,不具有继承特性,因此不能是虚函数。
8. 虚函数和纯虚函数的区别
(1)纯虚函数只有定义没有实现,虚函数有定义也有实现
(2)有纯虚函数的类不能定义对象
虚基类
当派生类从多个基类派生,而这些基类又有共同基类,则在访问此共同基类中的成员时,将产生冗余,并有可能因冗余带来不一致性。
以virtual说明基类继承方式
构造函数、析构函数、拷贝构造函数、赋值运算符
class class_name
{
private:
/* data */
public:
class_name(); // 构造函数
~class_name(); // 析构函数
class_name(const class_name &); //默认拷贝构造函数
class_name &operator=(const class_name &); //赋值运算符
};
1. 构造函数
是类的一种特殊的成员函数,会在每次创建类的新对象时执行。
构造函数的名称与类的名称完全相同,没有任何返回类型,也不会返回void。
默认构造函数: 没有参数, or 所有参数都有默认值
1.1. 执行顺序
(1)虚拟基类的构造函数(多个虚拟基类则按照继承的顺序执行构造函数)。
(2)基类的构造函数(多个普通基类也按照继承的顺序执行构造函数)
(3)类类型的成员对象的构造函数(按照初始化顺序)
(4)派生类自己的构造函数。
1.2. 什么时候会自动生成默认构造函数?
什么情况下C++编译器会生成默认的构造函数_如果没有子自定义构造函数,则系统会生成一个默认构造函数-CSDN博客
(1)含有类成员对象,该类成员对象有默认构造函数。
class A{
public:
A(){ // 默认构造函数
cout << "A()" << endl;
}
};
class B{
public:
A a; //成员对象
int num;
};
(2)基类带有默认构造函数的派生类。
class Base{
public:
Base(){
cout << "Base()" << endl;
}
};
class Derived :public Base{
public:
int d;
};
(3)带有一个虚函数的类
类本身定义了自己的虚函数,or,继承了虚函数
(4)带有虚基类的类
1.3. 构造函数的执行算法
(1)如果是在派生类构造函数中,则所有的虚基类及上一层基类的构造函数调用
(2)对象的vptr被初始化
(3)如果有成员初始化列表,将在构造函数体内扩展开
(4)执行程序员提供代码
2. 析构函数
在每次删除创建的对象的时候执行。
名称与类的名称相同,只是前面加了一个波浪号。
没有返回值。
没有参数。
2.1. 执行顺序
(1)派生类的析构函数
(2)成员类对象的析构函数
(3)基类的析构函数
2.2. 类什么时候析构?
(1)对象生命周期结束,被销毁时。
(2)delete指向对象的指针时,or ,delete指向对象的基类指针时,并且基类析构函数是虚函数时。
(3)对象A是对象B的成员,B的析构函数被调用时,也会调用A的析构函数。
3. 拷贝构造函数
新建一个对象并将初始化为同类现有对象时调用。
class_name(const class_name &); //默认拷贝构造函数
3.1. 什么时候使用?
- 将新对象初始化为一个同类对象
- 按值将对象传递给函数
- 函数按值返回对象
- 编译器生成临时对象
4.赋值运算符
class_name &operator=(const class_name &); //赋值运算符
用于处理同类之间的赋值。
5. 地址运算符
将已有的对象赋值给另一个对象时。
常见问题
1. 什么时候调用?
构造函数:创建类对象,or 显示调用
析构函数:对象过期时
2. 构造函数、析构函数是否可以抛出异常?*
(1)C++只会析构已经完成的对象,对象只有在其析构函数执行完毕才算是完成构造妥当。当构造函数中发生异常,控制权会转出构造函数之外。因此,在对象的构造函数中发生异常,其析构函数不会被调用,会发生内存泄漏。
(2)用auto_ptr对象来取代指针类成员,便对构造函数做了强化,免除了抛出异常时发生资源泄露的危机,不需要在析构函数中手动释放资源。
(3)如果控制权基于异常的元素离开西沟元素,而此时有另一个异常处于作用状态,C++会调用terminate函数让程序结束
(4)如果异常从析构函数抛出,而且没有在当地完成捕捉,那个析构函数便是执行不全的。如果析构函数执行不全,那就是没有完成他应该执行的每一件事情。
3. 为什么拷贝构造函数必须传引用不能传值?
4. 如何禁止自动生成拷贝构造函数。
何时需要合成复制构造函数
构造函数的扩展过程
程序员定义的析构函数被扩展的过程
C++如何阻止一个类被实例化?一般在什么时候将构造函数声明为 private?
(1) 将类定义为抽象基类 或 将构造函数声明为private
(2) 不允许类外部创建类对象,只能在类内部创建对象。
C++三大特性
1. 封装
隐藏具体实现细节。把客观事物抽象为抽象的类,并且类把自己的数据和方法只让可信的类和对象访问,对一些数据和方法进行隐藏。
人们可以使用类方法的共有接口对类对象执行操作,这是抽象。
类的数据成员可以是private,这意味着只能通过成员函数来访问这些数据,这就是数据隐藏。
2. 继承
从已有的类派生出新的类,而派生的类继承了原有类的属性和方法。
2.1. 继承
继承 | public | protected | private |
---|---|---|---|
public | public | protected | private |
protected | protected | protected | private |
private | 只能通过基类接口访问 | 只能通过基类接口访问 | 只能通过基类接口访问 |
能否隐式向上转换 | 是 | 是(只在派生类中) | 否 |
2.2. 派生类能从基类继承什么?
基类公有的数据和方法。
2.3. 不能继承什么?
构造函数、析构函数、赋 值运算函数、友元函数
3. 多态
同一事物表现出不同事物的能力,即向不同的对象发送同一条消息,接受后产生不同的行为。
重载实现编译时多态,虚函数实现运行时多态。
重载overload : 允许存在多个同名函数,具有不同的参数表(参数的类型、个数、顺序)
重写(覆盖)override: 子类重新定义父类虚函数的做法。
隐藏: 指派生类的函数屏蔽了与其同名的基类函数,注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。
重载
(1)只能重载已有的运算符,不创建新的运算符
(2)当重载的运算符是成员函数时,至少有一个操作数是用户自定义的类型
(3)不修改优先级
(4)不重载:
sizeof | |
---|---|
. | 成员运算符 |
.** | 成员指针运算符 |
:: | 作用域解析 |
?: | 条件运算符 |
typeid | RTTI |
const_cast、dynamic_cast、reinterpret_cast、static_cast | 强制类型转换运算符 |
(5)大多数运算符都可以通过成员函数或非成员函数重载,以下只能通过成员函数重载:
= | ( ) | [ ] | -> |
---|
C++11运算符重载详解与向量类重载实例(<<,>>,+,-,*等) - 进击的汪sir - 博客园 (cnblogs.com)
类成员的初始化方式
(1)赋值初始化
: 通过在函数体内进行赋值初始化。
class Student
{
public:
Student(string in_name, int in_age)
{
name = in_name;
age = in_age;
}
private :
string name;
int age;
};
(2)列表初始化
: 在冒号后使用初始化列表进行初始化。
class Student
{
public:
Student(string in_name, int in_age):name(in_name),age(in_age) { }
private :
string name;
int age;
};
(3)区别:
在函数体内初始化,实在所有的数据成员被分配内存空间后进行的。
列表初始化是给数据成员分配内存空间时进行初始化。
初始化列表的顺序是由类中声明顺序决定的,不是由初始化列表决定的。 ⭐
class Base
{
public:
Base(int i) : m_j(i), m_i(m_j) {} //初始化列表顺序
int get_i() const
{
return m_i;
}
int get_j() const
{
return m_j;
}
private:
int m_i; //声明顺序
int m_j;
};
int main()
{
Base obj(98);
cout << obj.get_i() << endl << obj.get_j() << endl; // 随机数 98
return 0;
}
(4) 什么时候需要成员初始化列表?过程是什么?
- 当初始化一个引用成员变量时;
- 初始化一个const成员变量时;
- 当调用一个基类的构造函数,而构造函数拥有一组参数时;
- 当调用一个成员类的构造函数,而它拥有一组参数
- 编译器会一一操作初始化列表,以适当顺序在构造函数之内安插初始化操作,并且在任何显示用户代码前。list中的项目顺序是由类中成员声明顺序决定的,不是由初始化列表中的排列顺序决定的。
(5)在成员函数中调用 delete this 会出现什么问题?对象还可以使用吗?
-
在类对象的内存空间中,只有数据成员和虚函数表指针,并不包含代码内容,类的 成员函数单独放在代码段中。在调用成员函数时,隐含传递一个 this 指针,让成员 函数知道当前是哪个对象在调用它。当调用 delete this 时,类对象的内存空间被 释放。在 delete this 之后进行的其他任何函数调用,只要不涉及到 this 指针的内 容,都能够正常运行。一旦涉及到 this 指针,如操作数据成员,调用虚函数等,就 会出现不可预期的问题。
-
为什么是不可预期的问题?
delete t his 释放了类对象的内存空间,但是内存空间却并不是马上被回收到系统中,可能是 缓冲或者其他什么原因,导致这段内存空间暂时并没有被系统收回。此时这段内存 是可以访问的,你可以加上 100,加上 200,但是其中的值却是不确定的。当你获取 数据成员,可能得到的是一串很长的未初始化的随机数;访问虚函数表,指针无效 的可能性非常高,造成系统崩溃。
-
如果在类的析构函数中调用 delete this,会发生什么?
会导致堆栈溢出。原因很简单,delete 的本质是“为将被释放的内存调用一个或多 个析构函数,然后,释放内存”。显然,delete this 会去调用本对象的析构函数, 而析构函数中又调用 delete this,形成无限递归,造成堆栈溢出,系统崩溃。
空类
1.空类的大小是多少?
-
C++空类的大小不为0, 不同编译器的设置不一样。
-
C++标准指出,不允许一个对象(当然包含类对象)的大小为0,不同的对象不能有相同的地址。
-
带有虚函数的C++类大小不为1,因为每个对象会有一个vptr指向虚函数表,具体大小根据指针大小确定。
-
C++要求对于类的每个实例都具有独一无二的地址,那么编译器自动复位空位分配一个字节的大小,这样便保证了每个实例都具有独一无二的地址。
2.空类会默认添加哪些东西?
一个空类,编译器会自动声明:
- 默认构造函数(如果是非空类,只在没有自定义任何构造函数时,才会由编译器补充)
- 拷贝构造函数
- 拷贝赋值运算符
- 析构函数
所有这些函数都是public且inline
静态类成员函数
不能通过对象调用静态成员函数。不能使用this指针。
静态成员函数不与特定的对象关联,所以只能使用静态数据成员。
友元函数为什么必须在类内声明
友元函数:允许非成员函数访问私有数据。
- 在类的声明中
- 不是成员函数,无需
::
类对象的大小?
(1)类的非静态成员变量大小,静态成员不占据类的空间,成员函数也不占据类的空间大小;
(2) 内存对齐另外分配的空间大小,类内的数据也是需要进行内存对齐操作的;
(3)虚函数的话,会在类对象插入 vptr 指针,加上指针大小;
(4)当该类是某类的派生类,那么派生类继承的基类部分的数据成员也会存在在派 生类中的空间中,也会对派生类进行扩展。
为什么要用基类指针指向派生类?
[为什么要用基类指针指向派生类对象?-CSDN博客](https://blog.csdn.net/hk121/article/details/81165391#:~:text=在基类与派生类之间,有一个 规定 :派生类对象的地址可以赋给指向基类对象的指针变量(简称,基类指针 ),即基类指针也可以指向派生类对象。 为什么有这一规定呢? 因为它可以实现多态性【1】,即向不同的对象发送同一个消息,不同的对象在接受时会产生不同的行为。)
模板
C++函数模板(模板函数)详解_c++模板函数-CSDN博客
1. 模板函数
函数模板不是一个实在的函数,编译器不能为其生成可执行代码。定义函数模板后只是一个对函数功能框架的描述,当它具体执行时,将根据传递的实际参数决定其功能。
template <typename 类型参数1, typename 类型参数2, ...>
返回值类型 模板名(形参表)
{
函数体
}
template <typename AnyType>
void Swap(AnyType &a,AnyType &b)
{
AnyType temp;
temp = a;
a = b;
b = temp;
}
int a = 1, b = 0;
Swap(a, b);
2. 模板类
类模板用于实现类所需数据的类型参数化
template <typename T> class TClass
{
// TClass的成员函数
private:
T Data;
}
#include<iostream>
using namespace std;
//A编程模板类--类型参数化
//类模板的定义 类模板的使用 类模板做函数参数
template <typename T>
class A{
public:
A(T a = 0)
{
this->a = a;
}
void printA()
{
cout << "a:" << a << endl;
}
private:
T a;
};
//参数 C++编译器具体的类
void UseA(A<int> &a)
{
a.printA();
}
void main()
{
//模板类本身就是抽象的,具体的类,具体的变量
A<int> a1(11),a2(22);//模板类是抽象的, 需要类型具体化
//a1.printA();
UseA(a1); //11
UseA(a2); //22
}
3. 模板类和模版函数区别
函数模版的实例化是由编译程序在处理函数调用时自动完成的,
而类模版的实例化必须由程序员在程序中显示的指定。
即函数模版允许隐式调用和显示调用; 但类模版只允许显示调用,使用时类模板必须加上
如stack
4.为什么模板类一般都放在一个.h 文件中 *
(1) 模板定义很特殊。由 template<…>处理的任何东西都意味着编译器在当时不为它分配 存储空间,它一直处于等待状态直到被一个模板实例告知。在编译器和连接器的某一 处,有一机制能去掉指定模板的多重定义。所以为了容易使用,几乎总是在头文件中 放置全部的模板声明和定义。
(2) 在分离式编译的环境下,编译器编译某一个.cpp 文件时并不知道另一个.cpp 文件的 存在,也不会去查找(当遇到未决符号时它会寄希望于连接器)。这种模式在没有模板 的情况下运行良好,但遇到模板时就傻眼了,因为模板仅在需要的时候才会实例化出 来,所以,当编译器只看到模板的声明时,它不能实例化该模板,只能创建一个具有 外部连接的符号并期待连接器能够将符号的地址决议出来。然而当实现该模板的.cpp 文件中没有用到模板的实例时,编译器懒得去实例化,所以,整个工程的.obj 中就找 不到一行模板实例的二进制代码,于是连接器也黔驴技穷了
5. 模板和实现可不可以不写在一个文件里面?为什么? *
因为在编译时模板并不能生成真正的二进制代码,而是在编译调用模板类或函数 的 CPP 文件时才会去找对应的模板声明和实现,在这种情况下编译器是不知道实现模板类或函数的 CPP 文件的存在,所以它只能找到模板类或函数的声明而找不到实现, 而只好创建一个符号寄希望于链接程序找地址。但模板类或函数的实现并不能被编译 成二进制代码,结果链接程序找不到地址只好报错了。
《C++编程思想》第15章(第300 页)说明了原因:模板定义很特殊。由template<…> 处理的任何东西都意味着编译器在当时不为它分配存储空间,它一直处于等待状态直到被一个模板实例告知。在编译器和连接器的某一处,有一机制能去掉指定模板的多 重定义。所以为了容易使用,几乎总是在头文件中放置全部的模板声明和定义。
6. 写一个比较大小的模板函数
template<typename type>
type Max(type A, type B)
{
return A > B? A : B;
}
int main()
{
int a = 100, b =99;
cout << Max(a, b);
return 0;
}
什么是组合?
(1)一个类里面的数据成员是另一个类的对象,即内嵌其他类的对象作为自己的成员;创建 组合类的对象:首先创建各个内嵌对象,难点在于构造函数的设计。创建对象时既要对 基本类型的成员进行初始化,又要对内嵌对象进行初始化。
(2) 创建组合类对象,构造函数的执行顺序:先调用内嵌对象的构造函数,然后按照内嵌对 象成员在组合类中的定义顺序,与组合类构造函数的初始化列表顺序无关。然后执行组 合类构造函数的函数体,析构函数调用顺序相反。
组合与继承优缺点?
(1)继承
继承是 Is a 的关系,比如说 Student 继承 Person,则说明 Student is a Person。
继承的优点: 是子类可以重写父类的方法来方便地实现对父类的扩展。
继承的缺点有以下几点:
①:父类的内部细节对子类是可见的。
②:子类从父类继承的方法在编译时就确定下来了,所以无法在运行期间改变从父类继 承的方法的行为。
③:如果对父类的方法做了修改的话(比如增加了一个参数),则子类的方法必须做出 相应的修改。所以说子类与父类是一种高耦合,违背了面向对象思想。
(2)组合
组合也就是设计类的时候把要组合的类的对象加入到该类中作为自己的成员变量。
组合的优点:
①:当前对象只能通过所包含的那个对象去调用其方法,所以所包含的对象的内部细节 对当前对象时不可见的。
②:当前对象与包含的对象是一个低耦合关系,如果修改包含对象的类中代码不需要修 改当前对象类的代码。
③:当前对象可以在运行时动态的绑定所包含的对象。可以通过 set 方法给所包含对象 赋值。
组合的缺点:
①:容易产生过多的对象。
②:为了能组合多个对象,必须仔细对接口进 行定义。
多继承的优缺点,作为一个开发者怎么看待多继承
(1)C++允许为一个派生类指定多个基类,这样的继承结构被称做多重继承。
(2) 多重继承的优点很明显,就是对象可以调用多个基类中的接口;
(3)如果派生类所继承的多个基类有相同的基类,而派生类对象需要调用这个祖先类的 接口方法,就会容易出现二义性
(4)加上全局符确定调用哪一份拷贝。比如 pa.Author::eat()调用属于 Author 的拷贝。
(5)使用虚拟继承,使得多重继承类 Programmer_Author 只拥有 Person 类的一份拷贝
设计一个类计算子类个数
- 为类设计一个 static 静态变量 count 作为计数器;
- 定义结束后初始化 count;
- ,设计拷贝构造函数,在进行拷贝构造函数中进行 count +1,操作;
- 设计复制构造函数,在进行复制函数中对 count+1 操作;
- 在析构函数中对 count 进行-1;
实现构造函数、析构函数、拷贝函数、赋值运算符
class String
{
public:
String(const char *str = NULL);
String(const String &other);
~String(void);
String &operate= (const String &other);
private:
char *m_data;
};
// 构造函数
String::String(const char *str)
{
if (str == NULL){
m_data = new char[1]; // 对空字符串⾃动申请存放结束标志'\0'
*m_data = '\0';
}
else{
int length = strlen(str);
m_data = new char[length + 1];
strcpy(m_data, str);
}
}
// 析构函数
String::~String(void)
{
delete[] m_data; // 或delete m_data;
}
// 拷⻉构造函数
String::String(const String &other)
{
int length = strlen(other.m_data);
m_data = new char[length + 1];
strcpy(m_data, other.m_data);
}
// 赋值函数
String &String::operate = (const String &other)
{
if (this == &other)
{
return *this; // 检查⾃赋值
}
delete[] m_data; // 释放原有的内存资源
int length = strlen(other.m_data);
m_data = new char[length + 1]; // 对m_data加NULL判断
strcpy(m_data, other.m_data);
return *this; // 返回本对象的引⽤
}
用 C 语言实现 C++的继承
继承机制中对象之间如何转换?指针和引用之间如何转换?
(1)向上类型转换
将派生类指针或引用转换为基类的指针或引用被称为向上类型转换,向上类型转换会 自动进行,而且向上类型转换是安全的。
(2)下类型转换
将基类指针或引用转换为派生类指针或引用被称为向下类型转换,向下类型转换不会 自动进行,因为一个基类对应几个派生类,所以向下类型转换时不知道对应哪个派生 类,所以在向下类型转换时必须加动态类型识别技术。RTTI 技术,用 dynamic_cast 进行向下类型转换。
标签:函数,对象,基类,C++,析构,派生类,构造函数 From: https://www.cnblogs.com/hedy77/p/18226079