C++面试题目整理持续(一)

本文深入探讨C++中的多态性概念,包括静态多态性和动态多态性,重点讲解虚函数和虚析构函数的使用,以及如何通过指针调用虚函数实现多态。同时,解析了纯虚函数、抽象类的概念,并讨论了深拷贝与浅拷贝的区别。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

C++ 面试题目整理

1. C 和C++ 最大的特点
(1) C++是面向对象的 三大特点:封装 继承 多态
(2) 引入引用代替指针
 (3)const /inline template 替代宏常量
2 C++多态 虚函数 虚析构函数
 1 多态:向不同对象发送同一个消息,不同的对象会产生不同的行为,发送消息可以是调用函数等操作。函数重载、运算符重载都是多态。
 2 静态多态性:函数重载和运算符重载(实质也是函数重载),编译时就知道调用哪个函数;
动态多态性:编译时不知道调用哪个函数,运行时才知道,是要通过虚函数实现的
3 .虚函数:当父类指针指向子类时,只能调用子类中的父类部分,但是如果父类中的某个成员函数被声明为虚函数时,该指针就可以调用子类中的子类部分了。实际上也是先指向子类中的父类部分,但是因为子类中的父类部分已经被声明为虚函数,所以不会被调用,而是调用子类中被重写的父类虚函数,就是取而代之了。对于虚函数都是通过指针、引用来调用的,通过对象名调用的话不用虚函数
 4 一个成员函数被声明为虚函数时,其子类中的同名函数都自动成为虚函数,不用每层声明都加virtual,但一般每层声明都加!
  5 父类中的非虚函数在子类中也可能被重新定义,如果使用父类指针调用就会调用父类的成员函数,如果使用子类指针调用就会调用子类的成员函数,这并不是多态,因为指针类型不同,跟虚函数无关。
  6 虚析构函数:一定要给多态继承的父类加上虚析构函数,原因是如果不这么做会造成delete其父类指针时造成诡异的对象局部销毁,而对象的销毁不完全,就是造成内存泄漏的原因。
虚析构函数一般都跟new delete有关,
e.g. 父类:point,子类:circle,
point *p=new circle;
delete p;
此时只会执行point的析构函数,不会执行circle的析构函数,会内存泄漏。
若声明point的析构函数为虚函数,则delete p会先执行circle的析构函数,再执行point的析构函数。
7 纯虚函数:父类中完全用不到,子类中能用到的函数。
定义方式:virtual 函数类型 函数名(参数列表)=0;
//没有函数体!!!
//“=0”只是一种形式,没啥意义;
//只有名字,没有功能,不能被调用,只是为了给子类做准备。

继承测试

创建子类对象
构造函数顺序 是先基类在 子类
析构函数顺序 是先 子类在基类

// 多态学习

class  Box
{
public:
	double width;
	double getLength();
	double getHeight();
	double getWidth();
	Box(double l, double h, double w) {
		this->length = l;
		this->width = w;
		this->height = h;
	}
protected:
	double height;
private:
	double length;
};

double Box::getLength() {
	return length;
}

double Box::getHeight() {
	return height;
}
double Box::getWidth() {
	return width;
}

class ColorBox :public Box {
private :
	int color;
public:
	int getColor();
	ColorBox(double l, double w, double h, int c) :Box(l, w, h) {
		this->color = c;
	}
};

int ColorBox::getColor() {
	return color;
}

int main()
{
     // 
	ColorBox b(0.2, 0.6, 0.5, 20);
	cout << "ColorBox--->width:" << b.getWidth() << endl;
	//通过类名作用域访问
	b.Box::width = 100.5;
	// 通过对象访问
	b.width = 100;
	cout << "Box----->width:" << b.Box::getWidth() << endl;
	//cout << b.width;
}

在这里插入图片描述

单一继承下的析构函数

说明
1、派生类与基类的析构函数没有什么联系,彼此独立,派生类或基类的析构函数只做各自类对象消亡前的清理工作;
2、在派生过程中,基类的析构函数不能继承,如果需要析构函数的话,就要在派生类中重新定义;
3、派生类析构函数的定义方法与没有继承关系的类中析构函数的定义方法完全相同,只要在函数体中负责把派生类新增的非对象成员的清理工作做好就够了,系统会自己调用基类及子对象的析构函数来对基类及子对象进行清理。
析构函数的执行顺序:
a、与构造函数的执行顺序正好相反
b、先析构派生类自己;
c、再析构各个子对象:如果有多个子对象,析构顺序与这些子对象在类中的说明次序相反;
d、最后才是析构基类。

派生类构造函数(总参数表) :基类1构造函数(参数表), ..., 基类n构造函数(参数表), 子对象1的构造函数(参数表), ..., 子对象k的构造函数(参数表)
{
	//派生类新增成员的初始化语句;
}

多重继承下派生类构造函数的执行顺序:

1)先构造基类:按照派生类定义时各基类在冒号后的声明顺序执行 ;

2)再构造子对象:按照各子对象在派生类的说明顺序执行 ;

3)最后才是构造派生类本身 。

当一个派生类是由多个基类派生而来时,如果这些基类中的成员有一些的名称相同,那么使用一个表达式引用了这些同名的成员,就会出现无法确定是引用哪个基类成员的情况,这就是对基类成员访问的二义性。
要避免此种情况,可以在成员名前用对象名及基类名来限定。

格式:
对象名.基类名::成员名
对象名.基类名::成员函数名(参数表)

支配规则:一个派生类中的名字将优先于它的基类中相同的名字,这时二者之间不存在二义性,当选择该名字时,使用支配者(派生类中)的名字,这称为支配规则。
可见性原则:在外层(基类)声明的标识符,如果在内层(派生类)没有同名的标识符,则它在内层(派生类)仍可见;如果内层(派生类)声明了同名的标识符,则外层(基类)的标识符不可见,即它被内层(派生类)同名标识符所覆盖。

虚基类
在这里插入图片描述
格式: class 派生类 :virtual[继承方式] 基类名
在这里插入图片描述
构造函数
1)要求虚基类的构造函数只能调用一次。直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化表中列出对虚基类的初始化。因此,要注意以下几点;
2)虚基类的构造函数在所有非虚基类之前调用;
3)若同一层次中包含多个虚基类,这些虚基类的构造函数按它们说明的次序调用;
4)若虚基类由非虚基派生而来,则先调用基类构造函数,再调用派生类的构造函数。
析构函数
调用顺序正好与它的构造函数调用顺序相反

多态性
顾名思义,多态就是“多种形态”的意思。它是面向对象程序设计的一个重要特征。
在面向对象方法中一般是这样表述多态性的:同样的消息被不同类型的对象接收时导致的不同行为。所谓消息是指对类的成员函数的调用,不同行为是指不同的实现,也就是调用了不同的函数。
多态的类型:
重载多态:普通函数或类的成员函数重载就属于这种类型
强制多态:强制数据类型的变化,以适用函数或操作的要求
包含多态:类族中定义于不同类的同名成员函数的多态行为,主要通过虚函数来实现
参数多态:类模板属于这种类型,使用实际的类型才能实例化

### 多态的实现
C++的多态性有两类:
1、静态多态性:也就是静态联编下实现的多态性,即是在程序编译时就能确定调用哪一个函数,函数重载和运算符重载实现的就是静态多态性;
2、动态多态性:也就是动态联编(虚函数)下实现的多态性,它只有在程序运行时才解决函数的调用问题,虚函数是实现动态多态性的基础
什么是联编 ?
又称为关联或绑定,是描述编译器决定在程序运行时,一个函数调用应执行哪段代码的一个术语,它把一个标识符与一个存储地址联系起来。

发生动态的条件:

1.继承
2.父类的引用或者指针指向子类的对象
3.函数的重写
虚函数
声明格式:

virtual 函数返回类型 函数名(形参表)
{
	//函数体
}

说明:
1、只有类的成员函数才能声明为虚函数,普通函数不存在继承关系,不能声明为虚函数;
2、virtual关键字出现在虚函数的声明处,在虚函数的类外定义时不加virtual;
3、静态成员函数不能声明为虚函数;
4、内联函数不能声明为虚函数;
5、构造函数也不能声明为虚函数,因为它是在对象产生之前运行的函
数;
6、析构函数可以是虚函数而且通常声明为虚函数。

// 多态学习
class Plane {
public:
	virtual void fly();
	virtual void land();
};
void Plane::fly() {
	cout << "起飞" << endl;
}
void Plane::land() {
	cout << "着陆" << endl;
}
class Jet :public Plane {
	virtual void fly();
	virtual void land();
};
void Jet::fly() {
	cout << "直升飞机在原地起飞..." << endl;
}
void Jet::land() {
	cout << "直升飞机降落在女神的屋顶..." << endl;
}

//喷气式飞机
class Copter :public Plane {
public:
	virtual void fly();
	virtual void land();
};

void Copter::fly() {
	cout << "喷气式飞机在跑道上起飞..." << endl;
}

void Copter::land() {
	cout << "喷气式飞机在跑道上降落..." << endl;
}

//          基类对象
void bizPlay(Plane& p) {
	p.fly();
	p.land();
}

void main() {

	Plane p1;
	bizPlay(p1);

	//绑定对象为 Jet
	Jet p2;
	bizPlay(p2);

	Copter p3;
	bizPlay(p3);
}

为什么要用指针->虚函数()方式而不是 对象.虚函数()方式来调用虚函数 ?

如果采用对象.虚函数()方式调用,只能得到一个个具体类的结果,不具备“跨类”功能。相反,指针则有“跨类”的能力,除此之外,引用也具备这种能力,以后我们将把指针、引用同等看待。

虚析构函数
为什么要引入虚析构函数 ?
用new命令建立派生类对象时返回的是派生类指针,根据赋值兼容规则,可以把派生类指针赋给基类指针。当用delete 基类指针 来删除派生类对象时,只调用基类的析构函数, 不能释放派生类对象自身占有的内存空间。
这一问题在引进虚析构函数后能够得到解决。

虚析构函数与一般虚函数的不同之处 :
当基类的析构函数被声明为虚函数时,它的派生类的析构函数也自动成为虚函数,这些析构函数不要求同名;
一个虚析构函数的版本被调用执行后,接着就要调用执行基类版本,依此类推,直到执行到派生序列的最开始的那个析构函数的版本为止,也即说派生类析构函数、基类析构函数能够依次被执行。
何时需要虚析构函数?
通过基类指针删除派生类对象时;
通过基类指针调用对象的析构函数。

抽象类:
1.当一个类具有一个纯虚函数,这个类就是抽象类
2.抽象类不能实例化对象
3.子类继承抽象类,必须要实现纯虚函数,如果没有,子类也是抽象类
抽象类的作用:为了继承约束,根本不知道未来的实现
抽象类不能用作参数类型、函数返回类型或强制类型转换,但可以声明抽象类的指针或引用

浅拷贝和深拷贝

如果在类的定义里没有提供拷贝构造函数,C++会提供一个默认的拷贝构造函数;
默认拷贝构造函数的拷贝方式是各成员逐一复制,这称为是浅拷贝;
在某些情况下,浅拷贝会产生问题,例如:当一个类在它的构造函数动态分配了内存资源,浅拷贝只会复制该资源的地址,而不会为新建的对象重新分配一个资源,因此,会出现多个对象指向同一个堆空间的地址的现象。

浅拷贝(值拷贝)问题

class Student
{
public:
	void mprintf(){
		cout << "name:" << name << "--------" << "age:" << age << endl;
	}
	//覆盖该类的默认构造函数
	Student(char*name, int age){
		this->name = (char*)malloc(100);
		strcpy(this->name, name);
		this->age = age;
		cout << "构造函数" << endl;
	}
	默认拷贝构造函数,就是值拷贝
	//Student(const Student &obj){
	//	this->name = obj.name;
	//	this->age = obj.age;
	//	cout << "拷贝构造函数" << endl;
	//}
	//析构函数
	~Student(){
		cout << "析构函数" << endl;
		free(this->name);
	}

private:
	char* name;
	int age;

};

void func(){
	Student s("xiaoMing", 15);
	Student s1 = s;
	s1.mprintf();
}

void main(){
	
	func();
	
	getchar();
}

C++ 类的静态成员
本文浅谈一下C++中类的静态成员,而类的静态成员有两种类型:
1、静态数据成员
2、静态成员函数
静态成员的适用场合:适合处理属于类中所有对象共有的数据,实现对象间的数据共享。
基本方法:在类的数据成员或成员函数前加上 static 关键字。
初始化格式: 数据类型名 类名::数据成员名[=初值];

**类外进行初始化**    
 eg :  int    类名:: 数据成员名字 = 初值
访问方式:
		通过对象来访问 
		通过类名 + 函数名来访问 

静态变量 必须在类外进行初始化 初始化进行后还可以进行 加减操作
const 常量必须类内初始化 常量初始化后不能进行操作

// 多态学习
class  Count {
public:
    int a;
    // 常量必须在类内进行初始化,常量不能进行操作
    //const static int total=10;
    // 静态成员变量必须在类外进行初始化
    // 可以进行操作 
    static int total;
public: 
    static void mycount() {
        total++;
        cout << "总数为" << total<<endl;
    }
    void addtest()
    {
       // total += 10;
        cout << total;
    }
    void print()
    {
        cout << a;
    }
};
int Count::total = 5;
int main()
{
    Count c;
    // 
    c.total = 10;
    // 只有静态成员才可以通过 类名进行访问
    //Count::print();
    c.mycount();
    Count::mycount();
}

在这里插入图片描述
说明:静态数据成员是在静态存储区分配空间,具有静态生命期,即存在于程序运行期间,直到程序运行结束,与对象的生、死无关。不分公有、私有,均在类外定义。
使用:
与普通的数据成员一样,成员函数可以在类内直接访问静态数据成员,即可以在构造函数、一般成员函数、析构函数、静态成员函数中访问静态成员。
对于公有的静态数据成员,还可以通过下列格式在类外调用:
类名::静态数据成员(推荐使用)

对象.静态数据成员(不推荐使用,这容易让人误以为静态数据成员依赖于对象,其实它与具体对象无关)
对于私有的静态数据成员,不允许在类外直接访问,但可以利用稍后介绍的公有静态成员函数来间接访问。
静态数据成员用得较多的场合:
保存流动变化的对象的个数(在构造函数、拷贝构造函数中对象个数增加1,在析构函数中对象个数减少1)

1)、静态成员函数不需要初始化;
2)、静态成员函数可以直接引用本类的静态数据成员;
3)、静态成员函数的设置主要是为了处理静态数据成员,通常设置为公有权限;
4)、静态成员函数没有this指针,不能直接访问普通的数据成员(即非静态数据成员),否则,将出错;
5)、静态成员函数可以用对象作为函数的参数,间接访问普通的数据成员(即非静态数据成员)。

提醒:对于公有的非静态成员函数,只能采用
对象.非静态成员函数([实参表]) 格式在类外调用,若采用 类名::非静态成员函数([实参表]) 格式调用,将出错!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值