点我–>C++语言基础
点我–>C++内存
点我–>STL
点我–>新特性
【操作系统】常见问题汇总及解答
【数据库】常见问题汇总及解答
【计算机网络】常见问题汇总及解答
【设计模式】总结篇
本文目录
- 1. 简述一下什么是面向对象
- 2. 简述一下面向对象的三大特征
- 3. 简述一下C++的重载和重写,以及它们的区别
- 4. 说说C++的重载和重写是如何实现的
- 5. 说说C语言如何实现C++中的重载
- 6. 说说构造函数有几种,分别有什么作用
- 7. 只定义析构函数,会自动生成哪些构造函数
- 8. 说说一个类,默认会生成哪些函数
- 9. 说说C++类对象的初始化顺序,有多重继承情况下的顺序
- 10. 简述向上转型和向下转型
- 11. 简述深拷贝和浅拷贝,如何实现深拷贝
- 12. 简述一下C++中的多态
- 13. 说说为什么要虚析构,为什么不能虚构造
- 14. 说说C++中的模板实例化和模板具体化
- 15. 说说类继承时,派生类对不同关键字修饰的基类方法的访问权限
- 16. 简述一下移动构造函数,什么库用到了这个函数
- 17. 请你回答一下C++类内可以定义引用数据成员吗
- 18. 说说构造函数为什么不能被声明为虚函数(13题)
- 19. 简述一下什么是常函数,有什么作用
- 20. 说说什么是虚继承,解决什么问题,如何实现
- 21. 简述一下虚函数和纯虚函数,以及实现原理
- 22. 说说纯虚函数能实例化吗,为什么;派生类要实现吗,为什么
- 23. 说说C++中虚函数与纯虚函数的区别
- 24. 说说C++中什么是菱形继承问题,如何解决
- 25. 请问构造函数中能不能调用虚方法
- 26. 请问拷贝构造函数的参数是什么传递方式,为什么
- 27. 说说类方法和数据的权限有哪几种
- 28. 说说如何理解抽象类
- 29. 说说什么是多态;除了虚函数,还有什么方式能实现多态
- 30. 简述一下虚析构函数,有什么作用
- 31. 说说什么是虚基类,可否被实例化
- 32. 简述一下拷贝赋值和移动赋值
- 33. 说说什么是仿函数,有什么作用
- 34. 说说C++ 中哪些函数不能被声明为虚函数
- 35. 说说C++中类模板和模板类的区别
- 36. 虚函数表里存放的内容是什么时候写进去的
1. 简述一下什么是面向对象
- 面向对象是一种编程思想,把一切东西看成是一个个对象,比如人、耳机、鼠标、水杯等,他们各自都有属性,比如:耳机是白色的,鼠标是黑色的,水杯是圆柱形的等等,把这些对象拥有的属性变量和操作这些属性变量的函数打包成一个类来表示。
- 面向过程和面向对象的区别
面向过程:根据业务逻辑从上到下写代码;
面向对象:将数据与函数绑定到一起,进行封装,这样能够更快速的开发程序,减少了重复代码的重写过程。
2. 简述一下面向对象的三大特征
面向对象的三大特征是:封装、继承、多态。
- 封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。对于类,不想给别人看到的,我们使用protected/private把成员封装起来。开放一些共有的成员函数对成员合理地访问。所以封装本质是一种管理。
- 继承:可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
继承方式 | private继承 | protected继承 | public继承 |
---|---|---|---|
基类的private成员 | 不可见 | 不可见 | 不可见 |
基类的protected成员 | 变为private成员 | 仍为protected成员 | 仍为protected成员 |
基类的public成员 | 变为private成员 | 变为protected成员 | 仍为public成员 |
- 多态:用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。实现多态,有二种方式:重写、重载。
3. 简述一下C++的重载和重写,以及它们的区别
- 重写:是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类对象调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有 virtual 修饰。
示例:
#include<iostream>
using namespace std;
class A
{
public:
virtual void fun()
{
cout << "A - func()" << endl;
}
};
class B : public A
{
public:
virtual void fun()
{
cout << "B - func()" << endl;
}
};
int main()
{
A* a = new B();
a->fun(); // B - func()
return 0;
}
- 重载:我们在平时写代码中会用到几个函数,他们的实现功能相同,但是有些细节却不同。例如:交换两个数的值其中包括(int、float、char、double)这些类型。在C语言中我们是利用不同的函数名来加以区分。这样的代码不美观而且给程序员也带来了很多的不便。于是在C++中人们提出了用一个函数名定义多个函数,也就是所谓的函数重载。函数重载是指同一可访问区内被声明的几个具有不同参数列(参数的类型、个数、顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。
示例:
#include<iostream>
using namespace std;
void func()
{
cout<< "this is func()" << endl;
}
void func(int a)
{
cout<< "this is func(int a)" << endl;
}
void func(double a)
{
cout<< "this is func(double a)" << endl;
}
void func(int a, double b)
{
cout<< "this is func(int a, double b)" << endl;
}
void func(double a, int b)
{
cout<< "this is func(double a, int b)" << endl;
}
int main()
{
func();
func(10);
func(3.14);
func(2, 3.4);
func(3.4, 2);
return 0;
}
4. 说说C++的重载和重写是如何实现的
- C++利用命名倾轧(name mangling)技术,来改名函数名,区分参数不同的同名函数。命名倾轧是在编译阶段完成的。
C++定义同名重载函数:
#include<iostream>
using namespace std;
int func(int a,double b)
{
return a + b;
}
int func(double a,float b)
{
return a + b;
}
int func(float a,int b)
{
return a + b;
}
int main()
{
cout << "func(3, 3.14) = " << func(3, 3.14) << endl; // 6
cout << "func(3.14, 2.f) = " << func(3.14, 2.f) << endl; // 5
cout << "func(2.f, 3) = " << func(2.f, 3) << endl; // 5
return 0;
}
- 在基类的函数前加上 virtual 关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。
(1)用 virtual 关键字声明的函数叫做虚函数,虚函数肯定是类的成员函数。
(2)存在虚函数的类都有一个一维的虚函数表叫做虚表,类的对象有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的。
(3)多态性是一个接口多种实现,是面向对象的核心,分为类的多态性和函数的多态性。
(4)重写用虚函数来实现,结合动态绑定。
(5)纯虚函数是虚函数再加上 = 0。
(6)抽象类是指包括至少一个纯虚函数的类。
纯虚函数:virtual void fun() = 0;
即抽象类必须在子类实现这个函数,即先有名称,没有内容,在派生类实现内容。
5. 说说C语言如何实现C++中的重载
C语言中不允许有同名函数,因为编译时函数命名是一样的,不像C++会添加参数类型和返回类型作为函数编译后的名称,进而实现重载。如果要用C语言显现函数重载,可通过以下方式来实现:
- 使用函数指针来实现,重载的函数不能使用同名称,只是类似地实现了函数重载功能;
- 重载函数使用可变参数,方式如打开文件open函数;
- gcc有内置函数,程序使用编译函数可以实现函数重载。
示例:
#include<stdio.h>
void func_int(void * a)
{
printf("%d\n",*(int*)a); //输出int类型,注意 void * 转化为int
}
void func_double(void * b)
{
printf("%.2f\n",*(double*)b);
}
typedef void (*ptr)(void *); //typedef申明一个函数指针
void c_func(ptr p,void *param)
{
p(param); //调用对应函数
}
int main()
{
int a = 23;
double b = 23.23;
c_func(func_int, &a);
c_func(func_double, &b);
return 0;
}
6. 说说构造函数有几种,分别有什么作用
C++中的构造函数可以分为4类:默认构造函数、初始化构造函数、拷贝构造函数、移动构造函数。
- 默认构造函数和初始化构造函数:在定义类的对象的时候,完成对象的初始化工作。有了有参构造,编译器就不提供默认的构造函数。
#include <iostream>
using namespace std;
class Student
{
public :
//默认构造函数
Student()
{
num = 1001;
age = 18;
}
//初始化构造函数
Student(int n, int a) :num(n), age(a) {}
private:
int num;
int age;
};
int main()
{
// 用默认构造函数初始化对象s1
Student s1;
// 用初始化构造函数初始化对象s2
Student s2(1002, 18);
return 0;
}
- 拷贝构造函数:赋值构造函数默认实现的是值拷贝(浅拷贝)。
#include <iostream>
using namespace std;
class Test
{
private:
int num;
int* p;
public:
Test(int x, int value)
{
num = x;
p = new int(value);
}
~Test()
{
delete p;
}
Test(const Test& t)
{
this->num = t.num;
this->p = new int(*t.p);
}
};
// 复制构造函数用于复制本类的对象
int main()
{
Test t1(1, 2);
Test t2(t1);// 将对象t1复制给t2,注意复制和赋值的概念不同
return 0;
}
- 移动构造函数:用于将其他类型的变量,隐式转换为本类对象。下面的转换构造函数,将int类型的r转换为Student类型的对象,对象的age为r,num为1004。
Student(int r)
{
int num = 1004;
int age = r;
}
7. 只定义析构函数,会自动生成哪些构造函数
在C++中,如果只定义了析构函数,编译器会自动生成默认构造函数、拷贝构造函数和移动构造函数。这是因为在C++中一个类必须有构造函数、拷贝构造函数和析构函数来创建、复制和销毁对象,否则会导致编译错误。
这里的默认构造函数指的是没有任何参数的构造函数,也被称为无参构造函数。当我们只提供了析构函数,编译器会自动为该类生成默认构造函数,以便能用于创建该类的对象。默认构造函数会调用对象的默认构造函数,即成员变量的构造函数,如果没有提供成员变量的构造函数,编译器还会为其生成默认构造函数。
拷贝构造函数和移动构造函数是用于对象复制和转移的函数,当我们只提供了析构函数时,编译器会自动生成默认的拷贝构造函数和移动构造函数。默认的移动构造函数则是将原对象的所有资源转移给目标对象,这在处理大对象时非常高效。
需要注意的是,如果我们手动定义了构造函数中的任一种(包括默认构造函数、拷贝构造函数和移动构造函数),编译器将不会自动生成该种构造函数。所以如果只定义了析构函数,通常需要手动定义一个默认构造函数,以确保类能够正常创建对象。
8. 说说一个类,默认会生成哪些函数
定义一个空类:
class Empty
{
};
默认会生成以下几个函数:
- 无参构造函数:在定义类的对象的时候,完成对象的初始化工作。
Empty() {}
- 拷贝构造函数:拷贝构造函数用于复制本类的对象。
Empty(const Empty& copy) {}
- 赋值运算符。
Empty& operator = (const Empty& copy) {}
- 析构函数(非虚)。
~Empty() {}
9. 说说C++类对象的初始化顺序,有多重继承情况下的顺序
- 创建派生类的对象,基类的构造函数优先被调用(也优先于派生类里的成员类);
- 如果类里面有成员类,成员类的构造函数优先被调用(也优先于该类本身的构造函数);
- 基类构造函数如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序,而不是它们在成员初始化表中的顺序;
- 成员类对象构造函数如果有多个成员类对象,则构造函数的调用顺序是对象在类中被声明的顺序而不是它们出现在成员初始化表中的顺序;
- 派生类构造函数,作为一般规则派生类构造函数应该不能直接向一个基类数据成员赋值,而是把值传递给适当的基类构造函数,否则两个类的实现变成紧耦合的(tightly coupled)将更加难以正确地修改或扩展基类的实现。(基类设计者的责任是提供一组适当的基类构造函数)
综上可以得出,初始化顺序:父类构造函数–>成员类对象构造函数–>自身构造函数。其中成员变量的初始化与声明顺序有关,构造函数的调用顺序是类派生列表中的顺序。析构顺序和构造顺序相反。
10. 简述向上转型和向下转型
向上转型 (Upcasting) 和向下转型 (Downcasting) 是面向对象编程中的两个重要概念。
向上转型是指将一个子类的对象转换为其父类类型的引用。这种转换是隐式的,可以自动进行。例如,假设有一个 Animal 类和一个 Cat 类,Cat 是 Animal 的子类,可以将 Cat 类型的对象转换为 Animal 类型的引用,如下所示:
Cat cat = new Cat();
Animal animal = cat; // 将 Cat 类型的对象转换为 Animal 类型的引用
向上转型常常用于多态性的实现。由于 Animal 类型的引用可以指向 Cat 类型的对象,因此可以在不知道对象类型的情况下调用 Animal 类型的方法,而这些方法可以在 Cat 类型的对象上执行。
向下转型是指将一个父类类型的引用转换为其子类类型的引用。这种转换是显式的,需要使用强制类型转换操作符进行,如下所示:
Animal animal = new Cat(); // 将 Cat 类型的对象转换为 Animal 类型的引用
Cat cat = (Cat) animal; // 将 Animal 类型的引用转换为 Cat 类型的引用
向下转型需要确保原始引用指向的对象实际上是目标类型,否则可能会出现 ClassCastException 异常。通常需要使用 instanceof 运算符来判断对象是否是目标类型:
if (animal instanceof Cat)
{
Cat cat = (Cat) animal;
// 转换成功,进行 Cat 类型的操作
}
else
{
// 转换失败,进行其他类型的操作
}
向下转型可以让我们使用子类特有的方法和属性,但需要谨慎使用,避免引起程序错误。
11. 简述深拷贝和浅拷贝,如何实现深拷贝
深拷贝和浅拷贝都是拷贝对象的内容(成员变量)的方式,但深拷贝和浅拷贝的效果是不同的。
- 浅拷贝:又称值拷贝,将源对象的值拷贝到目标对象中去,本质上来说源对象和目标对象共用一份实体,只是所引用的变量名不同,地址其实还是相同的。举个简单的例子,你的小名叫西西,大名叫冬冬,当别人叫你西西或者冬冬的时候你都会答应,这两个名字虽然不相同,但是都指的是你。
- 深拷贝:拷贝的时候先开辟出和源对象大小一样的空间,然后将源对象里的内容拷贝到目标对象中去,这样两个指针就指向了不同的内存位置。并且里面的内容是一样的,这样不但达到了我们想要的目的,还不会出现问题,两个指针先后去调用析构函数,分别释放自己所指向的位置。即为:每次增加一个指针,便申请一块新的内存,并让这个指针指向新的内存,深拷贝情况下,不会出现重复释放同一块内存的错误。
- 深拷贝的实现:实现深拷贝主要需要重载对象的拷贝构造函数和赋值运算符。在拷贝构造函数中,可以通过new为新对象的指针类型成员变量分配内存空间,并将原对象的指针所指向的内存区域复制到新的内存区域中。在赋值运算符中,同样需要为新对象的指针类型成员变量分配内存空间,并将原对象的指针所指向的内存区域复制到新的内存区域中,同时需要处理自我赋值问题和释放旧内存问题。
注意:进行深拷贝的对象必须满足两个条件:首先,对象必须具有可拷贝性;其次,对象中含有指针成员变量或其它需要在深拷贝时处理的成员变量。
深拷贝代码示例:
#include <iostream>
using namespace std;
class Person
{
public:
Person(const char* name, int age) : age(age)
{
//动态分配内存并复制name内容
this->name = new char[strlen(name) + 1];
strcpy(this->name, name);
}
//拷贝构造函数:完成深拷贝
Person(const Person& other) : age(other.age)
{
this->name = new char[strlen(other.name) + 1];
strcpy(this->name, other.name);
}
//赋值操作符重载:完成深拷贝
Person& operator=(const Person& other)
{
if (this == &other)
{
return *this;
}
this->age = other.age;
delete[] this->name;
this->name = new char[strlen(other.name) + 1];
strcpy(this->name, other.name);
return *this;
}
void printInfo() const
{
cout << "name: " << this->name << ", age: " << this->age << endl;
}
~Person()
{
delete[] this->name;
}
private:
char* name;
int age;
};
int main()
{
Person p1("Alice", 20);
Person p2 = p1; //调用拷贝构造函数实现深拷贝
Person p3("Bob", 30);
p3 = p1; //调用赋值操作符重载实现深拷贝
cout << "p1: ";
p1.printInfo();
cout << "p2: ";
p2.printInfo();
cout << "p3: ";
p3.printInfo();
return 0;
}
在上述示例中,Person类有一个指向动态分配内存的名为name的成员变量。为实现深拷贝,我们需要重载拷贝构造函数和赋值操作符重载,分别在拷贝时复制name指向的内存内容。具体而言,拷贝构造函数首先使用传入对象的name字符串长度创建一个新的内存块,然后将传入对象name的内容复制到新分配的内存块中。而赋值操作符重载可以分解为三个步骤:首先检查自赋值情况并返回当前对象的地址;其次释放当前对象name指向的内存空间,然后为当前对象分配新的内存空间并将传入对象name的内容复制到新分配的内存块中;最后返回当前对象的地址。在程序的主函数中,我们创建了三个Person类的实例,并通过拷贝构造函数和赋值操作符重载实现了深拷贝。最后将各个对象的信息打印出来,可以看到它们在内存地址上是不同的,并且它们的name成员变量存储的是不同的字符串内容。
12. 简述一下C++中的多态
C++中的多态是一种面向对象编程(OOP)的特性,它允许通过同一函数名来调用不同对象的方法或函数。由于派生类重写基类方法,然后用基类引用指向派生类对象,调用方法时会进行动态绑定,这就是多态。
在C++中,有两种类型的多态:运行时多态(动态多态) 和 编译时多态(静态多态)。
- 运行时多态:运行时多态在程序运行时根据对象的实际类型来调用相应的函数。多态的实现依靠虚函数(Virtual Functions)。虚函数是一种特殊类型的函数,被声明为虚函数的函数可以在派生类中被重新定义。在基类中声明虚函数的语法如下:
class Shape {
public:
virtual void draw() {
// Function definition
}
};
从上面的例子中可以看到,虚函数的声明包括关键字 virtual。这告诉编译器,这是一个虚函数,而不是普通的函数。
派生类可以重写基类中声明的虚函数,并且可以通过将该函数声明为 virtual 来实现多态性。例如:
class Circle : public Shape {
public:
void draw() {
// Function definition
}
};
在上面的例子中,Circle类重写了Shape类中的虚函数draw,因此可以使用多态,通过基类指针调用派生类的函数。
例如:
Shape* ptr = new Circle();
ptr->draw();
在上面的例子中,ptr指向一个Circle对象,但它是一个Shape类型的指针。由于Shape类中的draw函数是虚函数,因此会调用Circle类中的draw函数。
- 编译时多态:编译时多态是基于函数重载实现的。函数重载允许在同一作用域中声明多个具有相同名称但参数列表不同的函数。
例如:
void print(int num) {
cout << "integer: " << num << endl;
}
void print(float num) {
cout << "float: " << num << endl;
}
void print(char* str) {
cout << "string: " << str << endl;
}
这些函数都被命名为print,但它们接受不同类型的参数。编译器将根据参数类型调用适当的函数。
例如:
print(10); // Integer argument
print(2.123f); // Float argument
print("Hello, world!"); // C-style string argument
在编译时多态中,程序根据函数的参数类型决定要调用哪个函数。因此,它也被称为静态多态性或重载多态性。
13. 说说为什么要虚析构,为什么不能虚构造
为什么要虚析构:
将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们 new 一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。
因此,当一个类中存在虚函数时,为了实现正确的多态行为,通常也需要创建一个虚析构函数。通过将析构函数声明为虚函数,可以确保在删除派生类对象时调用正确的析构函数,从而释放对象占用的所有资源。如果没有虚析构函数,则只有基类析构函数被调用,而派生类专属的成员和资源则不能被正确地释放。
示例:
class Shape {
public:
virtual void draw() {
// Function definition
}
~Shape() {
// Function definition
}
};
class Circle : public Shape {
public:
void draw() {
// Function definition
}
~Circle() {
// Function definition
}
};
int main() {
Shape* ptr = new Circle();
delete ptr; // 调用Shape类中的析构函数,而不是Circle类中的析构函数
return 0;
}
在上面的例子中,Circle类是Shape类的派生类,并且它们都定义了析构函数。当使用基类指针指向派生类对象进行删除(delete)操作时,由于Shape类的析构函数不是虚函数,因此只调用Shape类中的析构函数,并没有调用Circle类中的析构函数,因此可能会导致派生类对象没有被完全销毁,从而出现内存泄漏等问题。
为什么会有这种情况呢?个人认为析构的时候如果没有虚函数的动态绑定功能,就只根据指针的类型来进行的,而不是根据指针绑定的对象来进行,所以只是调用了基类的析构函数;如果基类的析构函数是虚函数,则析构的时候就要根据指针绑定的对象来调用对应的析构函数了。
补充:C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。
为什么不能虚构造:
- 从存储空间角度:虚函数对应一个vtable,这个表的地址是存储在对象的内存空间的。如果将构造函数设置为虚函数,就需要到vtable中调用,可是对象还没有实例化,没有内存空间分配,如何调用。(悖论)
- 从使用角度:虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义。所以构造函数没有必要是虚函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
- 从实现上看:vbtl 在构造函数调用后才建立,因而构造函数不可能成为虚函数。从实际含义上看,在调用构造函数时还不能确定对象的真实类型(因为子类会调父类的构造函数);而且构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有太大的必要成为虚函数。
14. 说说C++中的模板实例化和模板具体化
在C++中,模板是一种通用的语法结构,可以用来创建泛型函数和类。模板可以根据传入的类型参数生成特定的代码,以适应不同的需求和数据类型。模板实例化和模板具体化是模板生成具体代码的两种方式。
模板实例化是编译器根据模板定义和具体的类型或值参数生成相应的代码的过程。例如,在下面的代码中,get_max
函数使用模板参数 T
来决定参数类型,并返回两个参数中的较大值:
template <typename T>
T get_max(T a, T b)
{
if (a > b) {
return a;
}
else {
return b;
}
}
int main()
{
int a = 42, b = 99;
double c = 3.14, d = 2.71;
int max_int = get_max(a, b); // instantiate get_max for int
double max_double = get_max(c, d); // instantiate get_max for double
return 0;
}
在上面的代码中,当编译器遇到 get_max(a, b)
和 get_max(c, d)
时,会根据实参的类型实例化 get_max
函数,生成两个版本的函数实例。
模板具体化是指在使用模板时,显式指定模板类型参数来生成具体的代码,以覆盖模板默认的行为。模板具体化可以分为两种类型:显式具体化(explicit specialization)和部分具体化(partial specialization)。
显式具体化通过特定的语法来指定模板类型参数,使得模板按照指定的类型参数生成特定的代码。它们的语法如下:
template <typename T>
void foo(T arg)
{
// 默认实现
}
template <>
void foo<int>(int arg)
{
// int类型的特定实现
}
上面的代码定义了一个名为 foo
的函数模板,它接受一个类型为 T
的参数。然后,我们使用显式具体化来指定了 foo
函数的 int
类型参数的实现,以便为 int
类型提供特定的实现。请注意,这里的<>
符号是显式具体化的关键点。
部分具体化是指在模板参数为多个类型时,指定其中一个或多个模板类型参数,并使用特定的实现进行代码生成。部分具体化使用的语法如下:
template <typename T, typename U>
void bar(T arg1, U arg2)
{
// 默认实现
}
template <typename T>
void bar<T, bool>(T arg1, bool arg2)
{
// T和bool类型的部分特定实现
}
上面的代码定义了一个名为 bar
的函数模板,它接受两个类型为 T
和 U
的参数。然后,我们使用部分具体化来指定 bar
函数的 bool
类型参数的实现,以便为 T
和 bool
类型提供特定的实现。
总之,模板实例化和模板具体化是模板生成具体代码的两种方式,模板实例化是生成通用代码的标准方式,而模板具体化是为特定类型或参数提供自定义实现的一种方式。
15. 说说类继承时,派生类对不同关键字修饰的基类方法的访问权限
C++中,派生类继承了基类的所有成员,包括方法和变量。派生类可以通过访问控制符来控制对父类成员的访问权限。C++中有三种不同的访问控制符:public、protected、和private。这三种访问控制符定义了派生类对基类成员的访问权限。
public继承:公共继承表示派生类可以访问基类的public成员。对于protected成员,派生类可以访问基类中的同名成员,但不能访问基类中非同名的protected成员。对于private成员,派生类无法访问。
示例代码:
class Base {
public:
void public_method();
protected:
void protected_method();
private:
void private_method();
};
class Derived : public Base {
public:
void foo() {
public_method(); // 可以访问
protected_method(); // 可以访问
// private_method(); // 无法访问
}
};
protected继承:保护继承表示派生类可以访问基类的public和protected成员。对于private成员,派生类无法访问。
示例代码:
class Base {
public:
void public_method();
protected:
void protected_method();
private:
void private_method();
};
class Derived : protected Base {
public:
void foo() {
public_method(); // 可以访问
protected_method(); // 可以访问
// private_method(); // 无法访问
}
};
private继承:私有继承表示派生类可以访问基类的public和protected成员,但这些成员在派生类中是私有的。对于private成员,派生类无法访问。
示例代码:
class Base {
public:
void public_method();
protected:
void protected_method();
private:
void private_method();
};
class Derived : private Base {
public:
void foo() {
public_method(); // 可以访问,但是在Derived中是private的
protected_method(); // 可以访问,但是在Derived中是private的
// private_method(); // 无法访问
}
};
总结:
- public继承:派生类可以访问基类的public、protected成员,不可以访问基类的private成员;派生类对象可以访问基类的public成员,不可以访问基类的protected、private成员。
- protected继承:派生类可以访问基类的public、protected成员,不可以访问基类的private成员;派生类对象不可以访问基类的public、protected、private成员。
- private继承:派生类可以访问基类的public、protected成员,不可以访问基类的private成员;派生类对象不可以访问基类的public、protected、private成员。
16. 简述一下移动构造函数,什么库用到了这个函数
C++11中新增了移动构造函数。与拷贝类似,移动也使用一个对象的值设置另一个对象的值;但是,又与拷贝不同的是,移动实现的是对象值真实的转移(源对象到目的对象):源对象将丢失其内容,其内容将被目的对象占有。移动操作发生的时候,是当移动值的对象是未命名的对象的时候。这里未命名的对象就是那些临时变量,甚至都不会有名称。典型的未命名对象就是函数的返回值或者类型转换的对象。使用临时对象的值初始化另一个对象值,不会要求对对象的复制:因为临时对象不会有其它使用,因而,它的值可以被移动到目的对象。做到这些,就要使用移动构造函数和移动赋值:当使用一个临时变量对对象进行构造初始化的时候,调用移动构造函数。类似的,使用未命名的变量的值赋给一个对象时,调用移动赋值操作。
移动操作的概念对对象管理它们使用的存储空间很有用的,诸如对象使用 new 和 delete 分配内存的时候。在这类对象中,拷贝和移动是不同的操作:从A拷贝到B意味着,B分配了新内存,A的整个内容被拷贝到为B分配的新内存上。而从A移动到B意味着分配给A的内存转移给了B,没有分配新的内存,它仅仅包含简单地拷贝指针。
移动构造函数和赋值代码示例:
#include <iostream>
#include <string>
using namespace std;
class Example {
string* ptr;
public:
Example(const string& str) : ptr(new string(str)) {}
~Example()
{
delete ptr;
}
// 移动构造函数,参数x不能是const Pointer&& x,
// 因为要改变x的成员数据的值;
// C++98不支持,C++0x(C++11)支持
Example(Example&& x) : ptr(x.ptr)
{
x.ptr = nullptr;
}
// 移动赋值
Example & operator= (Example && x)
{
delete ptr;
ptr = x.ptr;
x.ptr = nullptr;
return *this;
}
// access content :
const string & content() const
{
return *ptr;
}
// addition:
Example operator+(const Example& rhs)
{
return Example(content() + rhs.content());
}
};
int main() {
Example foo("Exam"); // 构造函数
// Example bar = Example("ple"); // 拷贝构造函数
Example bar(move(foo)); // 移动构造函数
// 调用move之后,foo变为一个右值引用变量,
// 此时,foo所指向的字符串已经被"掏空",
// 所以此时不能再调用foo
bar = bar + bar; // 移动赋值,在这儿"="号右边的加法操作,
// 产生一个临时值,即一个右值
// 所以此时调用移动赋值语句
cout << "foo's content: " << foo.content() << endl; // foo's content: Example
return 0;
}
17. 请你回答一下C++类内可以定义引用数据成员吗
C++类内可以定义引用成员变量,但要遵循以下三个规则:
- 不能用默认构造函数初始化,必须提供构造函数来初始化引用成员变量。否则会造成引用未初始化错误;
- 构造函数的形参也必须是引用类型;
- 不能在构造函数里初始化,必须在初始化列表中进行初始化。
代码示例:
class MyClass {
public:
MyClass(int& ref) : myRef(ref) { }
private:
int& myRef;
};
在此示例中,MyClass 类包含一个名为 myRef 的引用数据成员,该引用被初始化为一个 int 类型的对象的引用。在构造函数的成员初始化列表中,通过参数传递一个 int 引用来初始化 myRef 引用数据成员。
18. 说说构造函数为什么不能被声明为虚函数(13题)
ChatGPT给出的答案:
在C++中,构造函数不能被声明为虚函数。这是因为在创建对象时,虚函数的机制还没有启动任何虚表(v-table)或虚指针(v-pointer),它只是在分配内存和初始化构造函数的过程中完成的。因此,无法通过虚函数来实现多态性。
虚函数的作用是在运行时实现多态性,即在基类指针或引用指向派生类对象时,调用相应派生类的虚函数。但是,在构造函数中,对象还没有创建出来,无法涉及多态性。此外,如果将构造函数声明为虚函数,会导致一些问题,比如:构造函数无法被静态分配,因为虚函数机制需要在运行时才能确定调用的最终函数。
虽然不能将构造函数声明为虚函数,但派生类可以通过调用基类的构造函数来初始化从基类继承的成员变量和方法。这种方式可以实现多态行为。例如:
class Base {
public:
Base(int i) : num(i) {}
protected:
int num;
};
class Derived : public Base {
public:
Derived(int i, double d) : Base(i), data(d) {}
private:
double data;
};
在这个例子中,派生类 Derived 的构造函数使用基类的构造函数来初始化从基类继承的成员变量,同时还初始化了其自己的成员变量 data。
19. 简述一下什么是常函数,有什么作用
类的成员函数后面加 const,表明这个函数不会对这个类对象的数据成员(准确地说是非静态数据成员)作任何改变。在设计类的时候,一个原则就是对于不改变数据成员的成员函数都要在后面加 const,而对于改变数据成员的成员函数不能加 const。所以 const 关键字对成员函数的行为作了更明确的限定:有 const 修饰的成员函数(指 const 放在函数参数表的后面,而不是在函数前面或者参数表内),只能读取数据成员,不能改变数据成员;没有 const 修饰的成员函数,对数据成员则是可读可写的。除此之外,在类的成员函数后面加 const 还有什么好处呢?那就是常量(即 const)对象可以调用 const 成员函数,而不能调用非 const 修饰的函数。正如非 const 类型的数据可以给 const 类型的变量赋值一样,反之则不成立。
常函数的语法如下:
class MyClass {
public:
void myFunction() const {
// 只能访问成员变量,不能修改它们的值
}
};
20. 说说什么是虚继承,解决什么问题,如何实现
在C++中,虚继承是一种用于解决由于多重继承导致的二义性问题的技术。在多重继承中,如果两个或更多的基类有相同的派生类,派生类就具有多个相同的基类子对象,这就会导致二义性问题。为了解决这个问题,C++提供了虚继承机制。
虚继承通过共享相同的基类子对象来解决二义性问题,使多个派生类只继承一个共同的基类(称为虚基类),从而避免了派生类中的多个副本。当派生类通过虚继承继承一个虚基类时,其共享的子对象只有一个,并且始终位于最派生类对象的起始地址处。
以下是虚继承的示例代码:
class Base {
public:
int a;
};
class Derived1 : public virtual Base {
public:
int b;
};
class Derived2 : public virtual Base {
public:
int c;
};
class MyClass : public Derived1, public Derived2 {
public:
int d;
};
在这个示例中,Base 是虚基类,Derived1 和 Derived2 通过虚继承继承了 Base。MyClass 类从 Derived1 和 Derived2 派生而来,也是通过虚继承继承了 Base。这样做的好处是,MyClass 拥有 a、b、c 和 d 四个成员变量,但是只包含一个 Base 类型的子对象,避免了二义性问题。
虚继承的原理是,虚基类的子对象被放置在由派生类最后创建的位置上。这样可以确保所有派生类共享同一个虚基类。预先给出的示例会使 MyClass 中包含一个共同的子对象 Base,其顺序为: Base(虚基类子对象)、Derived1(虚基类子对象)、Derived2(虚基类子对象)、d。
总之,虚继承是C++中用于解决多重继承导致的二义性问题的一种技术,能够帮助编程人员编写更加可维护、可扩展的程序。
21. 简述一下虚函数和纯虚函数,以及实现原理
在C++中,虚函数和纯虚函数都是用于实现多态性的重要机制。
虚函数是通过在基类中声明并在派生类中重写实现的函数。通过将函数标记为虚函数,可以在派生类对象上调用该函数时,调用其相应的派生类版本,而不是基类版本。这种行为称为延迟绑定(Late Binding),也称为动态绑定(Dynamic Binding)。具体实现是,对于每个包含虚函数的类,编译器都会为其生成一个虚表(v-table),其中保存了指向各个虚函数的指针,每个对象都有一个指向其类的虚表的指针,从而实现多态性。
以下是虚函数的示例代码:
#include <iostream>
using namespace std;
class Base {
public:
virtual void print() {
cout << "This is base class." << endl;
}
};
class Derived : public Base {
public:
void print() override { // 可以不写override
cout << "This is derived class." << endl;
}
};
int main() {
Base* base = new Base;
Derived* derived = new Derived;
base->print(); // 输出 This is base class.
derived->print(); // 输出 This is derived class.
delete base;
delete derived;
return 0;
}
在此示例中,Base 类和 Derived 类都包含 print() 函数,print() 函数被声明为虚函数,因此可以在派生类对象上调用其相应的派生类版本。派生类 Derived 中重写了基类 Base 中的 print() 函数,发生多态性。
在虚函数的基础上,纯虚函数是指没有实现的虚函数,由于它们没有实现,因此不能被实例化。纯虚函数通常在抽象类中定义,抽象类是一种不能被直接实例化的类,仅仅用于作为其他类的基类。定义纯虚函数的语法如下:
virtual void myFunction() = 0;
在这个示例代码中,myFunction() 被声明为纯虚函数,通过在函数声明中加上 = 0,它变为纯虚函数。凡是包含了纯虚函数的类,被称为抽象类。
实现原理上,包含虚函数的类通常通过虚表(v-table)来实现,而每个对象都有一个指向其类的虚表指针,用于查找虚表中的函数地址。而纯虚函数的实现是由派生类提供的,因此它们实际上是一个函数指针,指向由派生类提供的实现。因此,派生类必须提供纯虚函数所需的实现,否则将无法实例化派生类对象。
总之,虚函数和纯虚函数是实现多态性的重要机制,它们能够使代码更具有灵活性和可扩展性。虚函数和纯虚函数的实现依赖于虚表和虚指针,可以帮助编程人员开发更高效的程序。
【补充】C++的override是什么?
在C++中,override是一个关键字,它用于重写(override)基类中的虚函数。虚函数是一个在基类中声明的函数,可以被派生类中的同名函数覆盖或实现。当派生类中的虚函数与基类的虚函数具有相同的函数名、参数列表和常量属性时,可以使用override关键字来明确指示该函数是基类的虚函数的重写。
使用override的好处是可以提高代码的可读性和可维护性。它可以让代码更加清晰地表明函数是在重写基类的虚函数,而不是意外地创建了一个新的函数。
举个例子,假设我们有一个Animal基类和一个Dog派生类,Animal类有一个虚函数makeSound()。要在Dog类中重写这个函数并发出不同的声音,我们可以这样写:
class Animal {
public:
virtual void makeSound() const {
cout << "generic animal sound" << endl;
}
};
class Dog : public Animal {
public:
void makeSound() const override {
cout << "woof!" << endl;
}
};
在这个例子中,我们用override来表示makeSound()是Animal类中的虚函数的重写。如果我们不小心修改了函数名或参数列表等,编译器会提示错误,从而帮助我们防止潜在的错误。
需要注意的是,override关键字只适用于虚函数的重写,而不适用于非虚函数。此外,override关键字也只适用于派生类中的函数,而不适用于基类中的函数。如果将override用于非虚函数或基类中的函数,编译器会报错。
22. 说说纯虚函数能实例化吗,为什么;派生类要实现吗,为什么
- 纯虚函数不可以实例化,但是可以用其派生类实例化。示例如下:
#include<iostream>
using namespace std;
class Base
{
public:
virtual void func() = 0;
};
class Derived :public Base
{
public:
void func() override
{
cout << "I love cpp" << endl;
}
};
int main()
{
Base* b = new Derived();
b->func(); // I love cpp
return 0;
}
- 虚函数的原理采用 v-table。类中含有纯虚函数时,其v-table 不完全,有个空位。即“纯虚函数在类的v-table表中对应的表项被赋值为0。也就是指向一个不存在的函数。由于编译器绝对不允许有调用一个不存在的函数的可能,所以该类不能生成对象。在它的派生类中,除非重写此函数,否则也不能生成对象。”所以纯虚函数不能实例化。
- 纯虚函数是在基类中声明的虚函数,它要求任何派生类都要定义自己的实现方法,以实现多态性。
- 定义纯虚函数是为了实现一个接口,用来规范派生类的行为,也即规范继承这个类的程序员必须实现这个函数。派生类仅仅只是继承函数的接口。纯虚函数的意义在于,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但基类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。
23. 说说C++中虚函数与纯虚函数的区别
总体来说,虚函数和纯虚函数的主要区别在于是否需要提供默认实现。虚函数可以提供选项默认实现,但也可以被子类重写,而纯虚函数需要它的派生类提供实现,从而强制实现多态性和抽象化。
- 虚函数和纯虚函数可以定义在同一个类中,含有纯虚函数的类被称为抽象类,而只含有虚函数的类不能被称为抽象类。
- 虚函数可以被直接使用,也可以被子类重载以后,以多态的形式调用,而纯虚函数必须在子类中实现该函数才可以使用,因为纯虚函数在基类有声明而没有定义。
- 虚函数和纯虚函数都可以在子类中被重载,以多态的形式被调用。
- 虚函数和纯虚函数通常存在于抽象基类之中,被继承的子类重载,目的是提供一个统一的接口。
- 虚函数的定义形式:
virtual{};
纯虚函数的定义形式:virtual {} = 0;
在虚函数和纯虚函数的定义中不能有 static 标识符,原因很简单,被static修饰的函数在编译时要求前期绑定,然而虚函数却是动态绑定,而且被两者修饰的函数生命周期也不一样。
24. 说说C++中什么是菱形继承问题,如何解决
下面的图表可以用来解释菱形继承问题(Diamond Inheritance Problem):
假设我们有类B和类C,它们都继承了相同的类A。另外我们还有类D,类D通过多重继承机制继承了类B和类C。因为上述图表的形状类似于菱形,因此这个问题被形象地称为菱形继承问题。现在,我们将上面的图表翻译成具体的代码:
class A {
public:
int a;
};
class B : public A {
public:
int b;
};
class C : public A {
public:
int c;
};
class D : public B, public C {
public:
int d;
};
在这种继承结构中,B类和C类都继承自同样的A类,而D类又继承了B类和C类,这意味着D类中将拥有来自A类的两份成员变量,这就是所谓的“菱形继承问题”。
菱形继承问题会导致一些问题:
- 内存浪费:因为A类的成员变量在D类中会有两个副本,浪费内存。
- 二义性:假设A类的成员函数foo()在B和C类中被重载,如果在D类中调用foo(),就会产生二义性,不知道应该调用哪一个版本。
为了解决这些问题,C++提出了虚继承(Virtual Inheritance)的概念。虚继承使得派生类继承同一个基类的不同子类时只保留一份基类成员,从而避免了内存浪费问题,也避免了二义性问题。在上面的例子中,使用虚继承可以这样实现:
class A {
public:
int a;
};
class B: public virtual A {
public:
int b;
};
class C: public virtual A {
public:
int c;
};
class D: public B, public C {
public:
int d;
};
这样,B类和C类都使用了虚继承,继承自A类的成员就只有一份了。虚继承会让编译器为每个虚基类创建一个虚基类表,用来保存该虚基类在内存块中的位置,从而实现正确访问虚基类成员。
25. 请问构造函数中能不能调用虚方法
- 在C++中,构造函数可以调用虚函数,但它不总是安全的。在对象的构造过程中,对象的虚函数表指针可能还没有被正确初始化,因此调用的虚函数可能还没有被正确设置。
- 当在派生类的构造函数中调用基类的虚函数时,如果派生类的构造函数会覆盖基类的虚函数,那么在调用之前,派生类的虚函数表还没有被正确地构造出来,这可能会导致调用错误的函数实现。这个问题通常被称为“虚函数在构造函数中的早期绑定问题(Early Binding Problem)”。
- 另外,在构造函数中还可能存在其他问题,比如构造函数执行的顺序没有按照人们设想的顺序执行、构造函数中会使用到未初始化的对象等。因此,通常建议尽可能避免在构造函数中调用虚函数。如果必须在构造函数中调用虚函数,那么需要非常小心,确保所有的对象都已经被正确地构造出来,尤其是避免在对象初始化期间访问未初始化的成员变量。
另一种回答:
- 不要在构造函数中调用虚方法,从语法上讲,调用完全没有问题,但是从效果上看,往往不能达到需要的目的。
- 派生类对象构造期间进入基类的构造函数时,对象类型变成了基类类型,而不是派生类类型。同样,进入基类析构函数时,对象也是基类类型。
- 所以,虚函数始终仅仅调用基类的虚函数(如果是基类调用虚函数),不能达到多态的效果,所以放在构造函数中是没有意义的,而且往往不能达到本来想要的效果。
26. 请问拷贝构造函数的参数是什么传递方式,为什么
- 拷贝构造函数的参数是以常量引用(const reference)的方式传递的。
- 这是因为拷贝构造函数的主要作用是根据给定的对象创建一个新的对象,而将对象作为引用传递可以避免在函数调用中创建对象的副本,从而提高程序的性能。同时,将参数声明为常量可以确保在拷贝对象时保持其原始状态不变。
如果拷贝构造函数的参数使用传值方式,那么在函数调用时需要拷贝整个对象的数据,这会导致对象构造的开销增大,尤其是在对象较大的情况下。如果参数声明为非常量引用,则可能导致在拷贝对象时意外改变原始对象的状态,因为非常量引用可以用于修改引用的对象,而拷贝构造函数的目的是创建一个新的对象,所以应该避免改变原始对象的状态。 - 因此,以常量引用的方式传递拷贝构造函数参数可以保证程序的性能,并且确保在拷贝对象时不会改变原始对象的状态。
27. 说说类方法和数据的权限有哪几种
在C++中,类成员(即类方法和数据成员)默认情况下是私有的,这意味着它们只能被类内的其他成员访问。但是,C++提供了三种不同的访问权限修饰符,以便控制类成员的可访问性。
关键字 | 权限 |
---|---|
public(公有) | 公有成员可以被类的任何部分访问,包括类的外部 |
protected(受保护) | 受保护的成员可以被类的成员和它的子类的成员访问,但不能被类的外部访问 |
private(私有) | 私有成员只能被类的成员访问,它们对外部和子类是不可访问的 |
代码实例:
#include<iostream>
#include<string>
using namespace std;
class Person
{
public :
Person(const string& name, int age) : m_name(name), m_age(age){}
void ShowInfo()
{
cout << "姓名:" << m_name << endl;
cout << "年龄:" << m_age << endl;
}
protected:
string m_name; // 姓名
private:
int m_age; // 年龄
};
class Teacher : public Person
{
public :
Teacher(const string& name, int age, const string& title) : Person(name, age), m_title(title) {}
void ShowTeacherInfo()
{
ShowInfo(); // 正确,public属性子类可见
cout << "姓名:" << m_name << endl; // 正确,protected属性子类可见
// cout << "年龄:" << m_age << endl; // 错误,private属性子类不可见
cout << "职称:" << m_title << endl; // 正确,本类中可见自己的所有成员
}
private:
string m_title; // 职称
};
void test()
{
Person person("张三", 22);
person.ShowInfo(); // public属性,对外部可见
// cout << person.m_name << endl; // protected属性,对外部不可见
// cout << person.m_age << endl; // private属性,对外部不可见
}
int main()
{
test();
return 0;
}
28. 说说如何理解抽象类
- 抽象类的定义如下:
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”,有虚函数的类就叫做抽象类。 - 抽象类有如下几个特点:
1)抽象类只能用作其他类的基类,不能建立抽象类对象。
2)抽象类不能用作参数类型、函数返回类型或显式转换的类型。
3)可以定义指向抽象类的指针和引用,此指针可以指向它的派生类,进而实现多态性。
29. 说说什么是多态;除了虚函数,还有什么方式能实现多态
- 多态是面向对象的重要特性之一,它是一种行为的封装,就是不同对象对同一行为会有不同的状态。例如: : 学生和成人都去买票时,学生会打折,成人不会。
- 除了虚函数,C++ 语言还提供了以下两种实现多态性的方式:
- 函数重载(Function Overloading): 函数重载是指在同一作用域中,可以定义多个同名函数,这些函数的参数列表和个数不同。在函数调用时,根据函数的参数列表和个数来匹配对应的函数实现。
int add(int a, int b);
float add(float a, float b);
- 运算符重载(Operator Overloading): 运算符重载是指通过重载运算符来实现多态性。可以重载的运算符包括算数运算符、比较运算符、赋值运算符等。通过重载运算符可以定义类的操作,使得类的对象可以使用与内置类型相同的操作符进行值的操作。
class MyClass {
public:
MyClass operator+(const MyClass& other) const;
};
30. 简述一下虚析构函数,有什么作用
虚析构函数,是将基类的析构函数声明为 virtual。虚析构函数是一种特殊的析构函数,在类的层次继承中很常见。它与普通析构函数的区别在于,虚析构函数是通过虚函数表来调用的,它可以确保在子类对象被删除时,先调用子类的析构函数再调用父类的析构函数,从而避免内存泄漏和未定义的行为。
答案解析:
一般地,在类继承结构中,如果派生类中声明了构造函数,则必须考虑到父类的指针指向派生类对象调用析构函数时,只会调用到基类的析构函数。这就会导致派生类中内存资源没有正确释放的问题。而在基类的析构函数前加上关键字 virtual,就会产生虚析构函数,以确保在删除派生类对象时,使用正确的析构函数释放其内存资源。
以下是一个使用虚析构函数的例子:
#include<iostream>
using namespace std;
class MyBaseClass {
public:
virtual ~MyBaseClass() {
cout << "MyBaseClass destructor" << endl;
}
};
class MyDerivedClass : public MyBaseClass {
public:
~MyDerivedClass() {
cout << "MyDerivedClass destructor" << endl;
}
};
int main() {
MyBaseClass* myObject = new MyDerivedClass();
delete myObject; // 输出 "MyDerivedClass destructor" 和 "MyBaseClass destructor"
return 0;
}
在这个例子中,基类 MyBaseClass 中的析构函数被声明为虚函数。一旦创建一个对象后,被赋值给指向基类 MyBaseClass 的指针,它就能调用到正确的析构函数,即首先调用派生类 MyDerivedClass 中的析构函数,然后再依次调用基类 MyBaseClass 中的析构函数。这种虚析构函数的使用方法能够保证在删除派生类对象时释放它所占用的所有内存,从而避免内存泄漏的问题。
31. 说说什么是虚基类,可否被实例化
虚基类(Virtual Base Class)是在多重继承中使用的一种特殊的基类。在被继承的类前面加上 virtual 关键字,这时被继承的类称为虚基类。
虚基类在定义时需要在其基类列表中使用虚基类语法进行声明,示例代码如下:
class Base1 {...};
class Base2 : virtual public Base1 {...}; // Base1为虚基类
class Base3 : virtual public Base1 {...}; // Base1为虚基类
class Derived : public Base2, public Base3 {...};
在上述代码中,Base1 是虚基类,Derived 类同时从 Base2 和 Base3 继承,但由于 Base1 是虚基类,从而避免了 Derived 类中存在两份 Base1 实例的情况。
由于虚基类只保留一份实例,因此某些情况下虚基类可能会在派生类中没有被正确地初始化,从而导致程序错误,因此使用虚基类时需要非常小心。虚基类实例化时,只能在其派生类中被实例化,无法直接实例化虚基类。
32. 简述一下拷贝赋值和移动赋值
- 拷贝赋值是将一个对象的值复制到另一个对象中,即在内存中创建一个副本,将其赋给新的对象,新对象与原对象独立。拷贝赋值的语法为:
class MyClass {
public:
MyClass(const MyClass& other); // 拷贝构造函数
MyClass& operator=(const MyClass& other); // 拷贝赋值运算符
};
MyClass obj1;
MyClass obj2 = obj1; // 调用拷贝构造函数
MyClass obj3;
obj3 = obj1; // 调用拷贝赋值运算符
在进行拷贝构造函数和拷贝赋值运算符重载时,需要通过 const 引用来传入对象的参数,并在函数内部实现对象的深拷贝,即将原对象的内容逐一复制到新对象中。
- 移动赋值是将一个对象的资源(如内存,文件句柄等)转移给另一个对象,避免在内存中创建临时对象,提高程序的效率。移动赋值的语法为:
class MyClass {
public:
MyClass(MyClass&& other); // 移动构造函数
MyClass& operator=(MyClass&& other); // 移动赋值运算符
};
MyClass obj1;
MyClass obj2 = std::move(obj1); // 调用移动构造函数
MyClass obj3;
obj3 = std::move(obj1); // 调用移动赋值运算符
在进行移动构造函数和移动赋值运算符的重载时,需要将对象的参数设置为右值引用,即使用双 && 对象参数定义,并使用 std::move() 函数来实现资源的转移。
需要注意的是,当一个对象同时实现了拷贝和移动构造函数和赋值运算符的重载时,我们要根据具体的情况来选择使用哪种方式赋值,可以根据是否需要保留原对象进行选择。更常用的方法是采用 “三五规则”,即通过移动操作进行赋值,同时实现拷贝操作以确保代码的健壮性。
33. 说说什么是仿函数,有什么作用
C++中的仿函数(Functor)是一种特殊的对象,它是一个行为类似于函数的对象,具有可调用运算符()。可以将仿函数看作是一种能够被函数调用运算符调用的普通函数指针的替代品,但相对于函数指针,仿函数可以携带更多的状态。
在STL中,算法函数往往需要一个函数指针作为参数来实现特定的功能,但有时候这个函数指针所指向的函数并不是我们想要的,此时就需要使用仿函数,我们可以使用仿函数通过重载函数调用运算符()来实现自定义的操作,从而满足算法函数的要求。
仿函数可以用于STL中的排序(sort)、查找(find)、删除(remove)等算法函数的自定义操作,充分体现了C++语言的泛型编程思想和代码复用性。此外,仿函数还可以用于其他需要函数指针的场合。
以下是一个示例,演示如何使用自定义的仿函数对一个vector进行排序:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 自定义仿函数
class MyCompare {
public:
bool operator()(const int& a, const int& b) {
return a > b;
}
};
int main() {
vector<int> nums{ 5, 2, 8, 1, 9 };
// 使用自定义仿函数对vector进行排序
sort(nums.begin(), nums.end(), MyCompare());
for (const auto& i : nums) {
cout << i << " "; // 9 8 5 2 1
}
cout << endl;
return 0;
}
在以上示例中,通过定义MyCompare类并重载了运算符(),实现了对int类型元素的降序排序,该仿函数最终被传给了sort算法函数,从而达到了自定义排序的目的。
34. 说说C++ 中哪些函数不能被声明为虚函数
在 C++ 中,以下这些函数不能被声明为虚函数:
- 静态函数:静态函数是和类名绑定在一起的,它们不属于对象,因此不能被声明为虚函数。
- 友元函数:因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。
- 内联函数:内联函数是在编译时期展开,而虚函数的特性是运行时才动态联编,所以两者矛盾。
- 构造函数:构造函数用来创建一个新的对象,而虚函数的运行是建立在对象的基础上,在构造函数执行时,对象尚未形成,所以不能将构造函数定义为虚函数。
注意:虚函数只适用于成员函数,这些函数属于类的实例,并且可以在派生类中覆盖。因此,全局函数和静态成员函数也不能是虚函数。
35. 说说C++中类模板和模板类的区别
在C++中,模板是一种通用的编程机制,允许编写可以适用于多种数据类型的类、函数或变量。类模板和模板类是指两个概念,但它们表达的含义是一样的,指的都是使用模板机制来创建的类。类模板和模板类没有本质上的区别,只是称呼不同。下面是一些关于类模板和模板类的解释:
类模板:
- 一个通用的模板,用于创建一组参数化的类;
- 可以将模板类看作模板,模板参数是类型;
- 定义类模板可以使用关键字template,然后在类的定义中使用模板参数;
- 当实例化一个类模板时,需要提供实际的类型参数。
模板类:
- 由一个类模板实例化得到的类;
- 实例化时需要指定所需类型参数;
- 模板类中的成员函数和成员变量根据实例化时的类型参数来确定其具体类型;
- 模板类可以像普通的类一样定义和使用。
下面是一个使用类模板来创建模板类的示例:
template <typename T>
class Stack {
private:
T* stack;
int top;
int size;
public:
Stack(int size) {
stack = new T[size];
top = -1;
this->size = size;
}
void push(T value) {
if (top == size - 1) {
std::cout << "Stack is full\n";
}
else {
stack[++top] = value;
}
}
T pop() {
if (top == -1) {
std::cout << "Stack is empty\n";
return -1;
}
else {
return stack[top--];
}
}
};
在这个示例中,类模板Stack是一个通用的栈实现,可以用于任何类型的数据。当使用int类型实例化Stack时,将创建一个具体的模板类Stack,表示一个整型栈。
Stack<int> intStack(10);
intStack.push(5);
intStack.push(10);
std::cout << intStack.pop() << "\n"; // 输出10
在实例化时,需要指定类型参数。在这个示例中,指定了int类型作为类型参数,这使得每个栈对象都是一个由int类型参数实例化的模板类。
36. 虚函数表里存放的内容是什么时候写进去的
- 虚函数表是一个存储虚函数地址的数组,以NULL结尾。虚表(vftable)在编译阶段生成,对象内存空间开辟以后,写入对象中的 vfptr,然后调用构造函数。即:虚表在构造函数之前写入。
- 除了在构造函数之前写入之外,我们还需要考虑到虚表的二次写入机制,通过此机制让每个对象的虚表指针都能准确的指向到自己类的虚表,为实现动多态提供支持。