C++ 的缺陷与思考 (opens new window)

# std::cin 的返回值是什么?

std::cin 是对象,operator >>() 的返回值类型是 istream& ,大多数情况下返回 std::cin 的地址,遇到 EOF 时,返回 0。

# C++中子类重写父类的虚函数能改变其访问权限吗?

可以。

# c++ 中应该怎么样在类内定义常量?

类似 static const int a = 5;,只能定义基本类型。

# 函数名是函数的地址吗?

函数名并不是函数地址的代表,这种误解与数组名就是指针一样犯了相同的错误。函数名是函数实体的代表, 不是地址的代表,当然,你马上就会有疑问,平时我们不都是把函数名作为函数的地址吗? 是的,我可以告诉你,函数名可以作为函数的地址,但是,绝大多数人都忽略了一个条件, 从函数到指针的隐式转换是函数名在表达式中的行为,就是说,这个转换仅在表达式中才会发生, 这仅是函数名众多性质中的一个,而非本质,函数名的本质就是函数实体的代表。

到了C++,由于C++规定,非静态成员函数的左值不可获得,因此非静态成员函数不存在隐式左值转换, 即不存在像常规函数那样的从函数到指针的隐式转换,所以必须在非静态成员函数前使用&操作符才能获得地址

# C++ 函数函数参数该用引用还是指针?

通常来说,对于简单类型的参数,可以使用值传递;对于需要修改实参的情况,可以使用指针传递或引用传递; 对于需要在函数中创建新对象的情况,应该使用指针传递。

参数可能为 nullptr 的和需要修改指针的才传递指针,否则都使用引用。

设计C++函数传参时如何决定使用指针还是引用? (opens new window)

# 在getline之前,如果用了cin输入,getline会无效。

因为有一个回车键留在了缓冲区,而getline会直接使用这个缓冲区,geline 会读取空白符,所以就会读取 enter 回车并接收。

解决办法是在getline之前用cin.ignore();

cin.ignore()默认不给参数的话,默认参数为cin.ignore(1, EOF),即把 EOF 前的 1 个字符清掉,没有遇到EOF就清掉一个字符然后结束。

一般可以这样使用:cin.ignore(1024, '\n');,通常把第一个参数设置得足够大,这样实际上是为了只有第二个参数 \n 起作用,所以这一句就是把回车(包括回车)之前的所以字符从输入缓冲流中清除出去。

char a = cin.get() 用来获取输入的第一个字符。

cin.get(字符数组名,20)//20指的是输入字符串的个数

getline(cin, xxx) 属于 string 流,cin.getline() 属于 istream 流。

# c++ 怎么进行输出格式化?

#include <iostream>
#include <iomanip>
using namespace std;
int main()
{
    double i = 100 / 3.0;
    double you = 200 / 3.0;

    cout << i << endl << you << endl;

	// fixed使用小数计数法(而不是科学计数法)显示浮点数
	// setprecision(4) 小数部分保留4位,最后一位四舍五入
    // setw() 设置输出的总宽度,通常用来对齐显示数据。
    // setfill() 数字本身宽度不够,用填充字符填充显示。
    // boolalpha 打印 true/false
    cout << fixed << setprecision(3) << setfill('0') << setw(10)
         << i << endl << you << boolalpha << true << endl;

    cout.precision(3);

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 派生类和基类有同名函数,派生类怎么调用基类的同名函数?

class Person {
    void fun(){

    }
}

class Man : public Person {
    void fun() {

    }
}


void func() {
    Man man;
    // 这样调用。
    man.Person::fun():
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 怎么在 main 函数之前执行语句?

C++ 中可以在全局变量的构造函数中实现:

int x = printf("Hello, world");


int main(int argc, const char *argv[])
{
    return 0;
}
1
2
3
4
5
6
7

# Qt 中为什么要引入 QVariant?

因为 C++ 禁止联合 union 包含具有非默认构造函数或析构函数的类型:

#include <iostream>
class Greeting {
    int a;
    char b;
    Greeting (){}
};

typedef union _uni {
    Greeting  s;    // 错误
}uni;

int main(void)
{
    uni u;

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# Qt6 中怎么替代 Qt5 里的 exactMatch() ?

QRegularExpression regExp(QRegularExpression::anchoredPattern(QLatin1String("(.+)_v(\\d+)")));
QRegularExpressionMatch ma = regExp.match(xxx);
if (ma.hasMatch()) {

}
1
2
3
4
5

Port QRegExp::exactMatch() in Qt6 (opens new window)

# 函数参数中的数组变量会退化为指针。

void bubble_sort(int arr[], int n) {

}

// 等效于

void bubble_sort(int *arr, int n) {

}
1
2
3
4
5
6
7
8
9

# 为什么需要仿函数?

函数对象(仿函数)可以携带附加数据。

如果编程者要将某种“操作”当做算法的参数,一般有两种方法: (1)一个办法就是先将该“操作”设计为一个函数,再将函数指针当做算法的一个参数。上面的实例就是该做法; (2)将该“操作”设计为一个仿函数(就语言层面而言是个 class),再以该仿函数产生一个对象,并以此对象作为算法的一个参数。

很明显第二种方法会更优秀,因为第一种方法扩展性较差,当函数参数有所变化,则无法兼容旧的代码。在我们写代码时有时会发现有些功能代码,会不断地被使用。为了复用这些代码,实现为一个公共的函数是一个解决方法。不过函数用到的一些变量,可能是公共的全局变量。引入全局变量,容易出现同名冲突,不方便维护。

例子:

#include <iostream>
using namespace std;

class IsGreaterThanThresholdFunctor {
public:
	explicit IsGreaterThanThresholdFunctor(int t):threshold(t){}
	bool operator() (int num) const {
		return num > threshold ? true : false;
	}
private:
	const int threshold;
};

int RecallFunc(int *start, int *end, IsGreaterThanThresholdFunctor myFunctor) {
	int count = 0;
	for (int *i = start; i != end + 1; i++) {
		count = myFunctor(*i) ? count + 1 : count;
	}
	return count;
}

int main() {
	int a[5] = {10,100,11,5,19};
    // 将判定的阈值也作为一个变量传入
    // 普通的函数指针则不能传入变量,只能使用局部变量或者全局变量
	int result = RecallFunc(a, a + 4, IsGreaterThanThresholdFunctor(10));
	cout << result << endl;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# push_back 和 emplace_back 有什么区别?

emplace_back() 和 push_back() 的区别,就在于底层实现的机制不同。push_back() 向容器尾部 添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话, 事后会自行销毁先前创建的这个元素);而 emplace_back() 在实现时,则是直接在容器尾部创建 这个元素,省去了拷贝或移动元素的过程。

使用emplace_back() 函数可以减少一次拷贝或移动构造的过程,提升容器插入数据的效率。

emplace_back(构造参数列表)只调用一次构造函数,而push_back(构造参数列表)会调用一次构造函数+一次移动构造函数。其余情况下,二者相同。

# 为什么用了 explicit 不能用 push_back 却能用 emplace_back?

因为 explicit 禁用了隐式类型转换,但是 emplace_back() 并不会转换,而是直接使用参数原地调用构造函数。

class Flower{
private:
 int no = 0;
 string name = "";
public:
 explicit Flower(int no, string name):no(no), name(name){
     cout << "构造" << endl;
 }
 Flower(){
     cout << "无参构造" << endl;
 }
 ~Flower() = default;
 Flower(const Flower &) {
     cout << "拷贝构造" << endl;
 }
 Flower(Flower &&) {
     cout << "移动构造" << endl;
 }
 Flower& operator=(const Flower&) {
     cout << "赋值" << endl;
     return *this;
 }

};

int main(int argc, const char *argv[])
{
    vector<Flower> flowers;
    flowers.reserve(3);
    // 会构造一次
    flowers.emplace_back(0, "小菜花");
    cout << "----------------------------" << endl;
    // 会构造两次
    flowers.push_back(Flower(1, "小喇叭花"));
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

emplace 使用了std::forward() 传递参数。

# array、vector、数组有什么异同?

共同点 (1.)都和数组相似,都可以使用标准数组的表示方法来访问每个元素(array和vector都对下标运算符[ ]进行了重载) (2.)三者的存储都是连续的,可以进行随机访问 不同点 (0.)数组是不安全的,array和vector是比较安全的(有效的避免越界等问题) (1.)array对象和数组存储在相同的内存区域(栈)中,vector对象存储在自由存储区(堆) (2.)array可以将一个对象赋值给另一个array对象,但是数组不行 (3.)vector属于变长的容器,即可以根据数据的插入和删除重新构造容器容量;但是array和数组属于定长容器 (4.)vector和array提供了更好的数据访问机制,即可以使用front()和back()以及at()(at()可以避免a[-1]访问越界的问题)访问方式,使得访问更加安全。而数组只能通过下标访问,在写程序中很容易出现越界的错误 (5.)vector和array提供了更好的遍历机制,即有正向迭代器和反向迭代器 (6.)vector和array提供了size()和Empty(),而数组只能通过sizeof()/strlen()以及遍历计数来获取大小和是否为空 (7.)vector和array提供了两个容器对象的内容交换,即swap()的机制,而数组对于交换只能通过遍历的方式逐个交换元素 (8.)array提供了初始化所有成员的方法fill() (9.)由于vector的动态内存变化的机制,在插入和删除时,需要考虑迭代的是否有效问题 (10.)vector和array在声明变量后,在声明周期完成后,会自动地释放其所占用的内存。对于数组如果用 new[ ]/malloc 申请的空间,必须用对应的delete[ ]和free来释放内存。

【C++】array和vector,数组三者区别和联系 (opens new window)

# map 使用 [] 需要 value 有默认构造函数

#include <iostream>
#include <map>
#include <string>
using namespace std;

class student
{
    public:
        string name;
        int age;

        student(string n, int a)
        {
            this->name = n;
            this->age = a;
        }
};


int main()
{
    map<int, student> mp;
    // 运行时,下面的语句会报错,因为 student 没有默认构造函数。
    mp[1] = student("jack", 25);
    mp[2] = student("kate", 25);

    cout << mp[1].name << endl;

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

map 在编译阶段发现所对应的value不存在,会调用默认构造函数构造一个对象,如果没有,就会报错。

# 该不该写默认构造函数?

默认构造函数对于成员变量是自定义类型的,调用这个成员变量所在类的默认构造函数初始化,对于成员变量是基本类型的,C++没有规定(可以认为是随机值)。

《More Effective C++》建议,尽量不提供默认构造函数。

添加无意义的默认构造函数会影响效率。

缺乏默认构造函数的缺点:

  1. 无法定义数组。
  2. 不适用与许多基础模板容器类,如 map 的 operator[],要求 value 必须有默认构造函数。
  3. 虚基类如果没有默认构造函数,则子类无论距离多遥远,必须知道、提供虚基类的构造函数。

# 模板类怎么进行前向引用?

template<typename T, typename Y> class Map;


namespace std {
	template <class _Ty>
	class allocator;
	template <class _Ty, class _Alloc = allocator<_Ty>>
	class vector;
}
1
2
3
4
5
6
7
8
9

# QString 和 QByteArray 有什么区别?

QString是专门用来处理字符串的,除了能处理ASCII编码字符,还包括各国语言的编码, 默认情况下QString会把所有数据当做utf-8编码来处理。QByteArray只是单纯用来处理数据的, 除了能处理ASCII编码字符,其它复杂的编码不能处理,直接以字节流的方式来对待。

# TLS 是什么?

传输层安全性协议(英语:Transport Layer Security,缩写作TLS),及其前身 安全套接层(Secure Sockets Layer,缩写作SSL)是一种安全协议,目的是为互联网通信提供安全 及数据完整性保障。网景公司(Netscape)在1994年推出首版网页浏览器,网景导航者时,推出HTTPS协议 以SSL进行加密,这是SSL的起源。IETF将SSL进行标准化,1999年公布第一版TLS标准文件。随后又公布 RFC 5246 (2008年8月)与RFC 6176(2011年3月)。在浏览器、邮箱、即时通信、VoIP、网络传真等应用程序中, 广泛支持这个协议。主要的网站,如Google、Facebook等也以这个协议来创建安全连线,发送数据。 目前已成为互联网上保密通信的工业标准。

TLS 就是在数据到网络传输之前做了一层安全措施(加密),也就是安全传输层,Transport Layer Security。 而它的前身SSL,对HTTP协议加了密,变成了HTTPS。

http + ssl = https。

# Qt 无法连接 ssl 动态库

使用 Qt 进行 https 请求,运行时报错:qt.network.ssl: QSslSocket::connectToHostEncrypted: TLS initialization failed

打印

    qDebug()<<
        QSslSocket::sslLibraryBuildVersionNumber()		// 返回编译时SSL(静态库)版本号
             << QSslSocket::sslLibraryBuildVersionString()	// 返回编译时SSL(静态库)版本字符串
             << QSslSocket::sslLibraryVersionNumber()			// 返回运行时SSL库版本号
             << QSslSocket::sslLibraryVersionString()		// 返回运行时SSL库版本字符串
             << QSslSocket::supportsSsl();						// 返回是否支持SSL true支持/false不支持
// 打印结果:269488255 "OpenSSL 1.1.1g  21 Apr 2020" 0 "" false
1
2
3
4
5
6
7

可知:Qt 编译时提供了静态库,但是没有提供运行时链接的动态库。

解决qt.network.ssl: QSslSocket::connectToHostEncrypted: TLS initialization failed (opens new window)

原因是:

Qt二进制安装包包含了QtNetWork使用的OpenSSL库。不过,它不会自动部署到Qt构建的应用程序中。

美国对软件出口有限制,而 OpenSSL 就是设计加密的开源软件,所以在某些地区使用的时候会有限制。

解决方法是根据打印出来的编译时 ssl 库版本号区下载 ssl 源码自行编译后得到运行时库。

或者直接安装 openssl 库,拷贝 libcrypto-1_1.dll 和libssl-1_1.dll 到运行目录。

解决 QT 发送 HTTP 请求时遇到 qt.network.ssl: QSslSocket::connectToHostEncrypted: TLS initialization failed 问题 (opens new window)

# Qt是事件驱动的,这句话该怎么理解呢?

Qt将系统产生的信号(软件中断)转换成Qt事件,并且将事件封装成类,所有的事件类都是由QEvent派生的, 事件的产生和处理就是Qt程序的主轴,且伴随着整个程序的运行周期。因此我们说,Qt是事件驱动的。

# Qt事件是由谁接收的?

答:QObject!它是所有Qt类的基类!是Qt对象模型的核心!QObject类的三大核心功能其中之一 就是:事件处理。QObject通过event()函数调用获取事件。 所有的需要处理事件的类都必须继承自Qobject,可以通过重定义event()函数实现自定义事件处理或者将事件交给父类。

# 事件处理的流程是什么样的?

答:事件有别于信号的重要一点:事件是一个类对象具有特定的类型,事件 多数情况下是被分发到一个队列中(事件队列),当队列中有事件时就不停的将队列中的事件发送给 QObject对象,当队列为空时就阻塞地等待事件,这个过程就是事件循环!

QCoreApplication::exec()开启了这种循环,一直到QCoreApplication::exit()被调用才终止, 所以说事件循环是伴随着Qt程序的整个运行周期。

# Qt QNetworkAccessManager出现SslHandshakeFailedError 问题

QSslConfiguration config = QSslConfiguration::defaultConfiguration();
config.setProtocol(QSsl::AnyProtocol);
config.setPeerVerifyMode(QSslSocket::VerifyNone);
request.setSslConfiguration(config);
1
2
3
4

Qt QNetworkAccessManager出现SslHandshakeFailedError (opens new window)

# 为什么无法正常打印 Unicode 字符?

windows 平台使用 UTF-8 with BOM 格式编码,*nix 平台使用 UTF-8 格式。

「UTF-8」和「带 BOM 的 UTF-8」的区别就是有没有 BOM。即文件开头有没有 U+FEFF。

# 为什么 connect 中的 lambda 存在问题?

Lambda表达式的引用捕获是在被调用时复制,而值捕获是在Lambda表达式创建时就复制了, 一定要注意创建和调用之间,被捕获值是否发生了改变。

要注意引用的生命周期不要超过被引用对象的生命周期。

# 四则运算符的重载为什么不能返回引用?

这四个操作符没有side effect,因此,它们必须构造一个对象作为返回值。

静态对象的引用因为((a+b) == (c+d))会永远为true而导致错误,所以只能返回一个对象了。

# 结构与联合有和区别?

  • 结构和联合都是由多个不同的数据类型成员组成, 但在任何同一时刻, 联合中只存放了一个被选中的成员(所有成员共用一块地址空间), 而结构的所有成员都存在(不同成员的存放地址不同)。

  • 对于联合的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了, 而对于结构的不同成员赋值是互不影响的。

# 形式参数中定义对象会调用什么构造函数?

如果参数是引用传递,则不会调用任何构造函数;如果是按值传递,则调用复制构造函数,按参数的值构造一个 临时对象,这个临时对象仅仅在函数执行是存在,函数执行结束之后调用析构函数。

# 模板类/函数的定义和实现

模板类/函数的定义和实现不能分开写。因为实现文件不会生成模板函数的实例化模板函数,链接会出错。

template 处理的任何东西都意味着编译器当时不为它分配空间,知道模板实例被告知。

最好的方法是在头文件实现。

# 有了虚函数为什么还要引入纯虚函数?

有纯虚函数的类是抽象类,不能生成对象,只能派生。它派生的类的纯虚函数没有被改写,那么,它的派生类还是个抽象类。

定义纯虚函数就是为了让基类不可实例化,因为实例化这样的抽象数据结构本身并没有意义,或者给出实现也没有意义。

纯虚函数的引入,是出于两个目的:

  1. 为了安全,因为避免任何需要明确但是因为不小心而导致的未知的结果,提醒子类去做应做的实现。
  2. 为了效率,不是程序执行的效率,而是为了编码的效率。

# 什么是 ini 文件?

为什么要用INI文件?如果我们的程序没有任何配置文件时,这样的程序对外是全封闭的, 一旦程序需要修改一些参数必须要修改程序代码本身并重新编译,这样很不好,所以要用配置文件, 让程序出厂后还能根据需要进行必要的配置;配置文件有很多,如INI配置文件,XML配置文件, 还有就是可以使用系统注册表等

*.ini文件是Initialization file的缩写,即为初始化文件,是Windows系统配置文件所采用的 存储格式,统管Windows的各项配置,一般用户就用Windows提供的各项图形化管理界面就可以实现 相同的配置了。但在某些情况,还是要直接编辑ini才方便,一般只有很熟悉Windows才能去直接编辑。

“.ini”文件由节、键、值组成,一个.ini”文件通常包括多个节(section),每个节包含不同的键(key)及对应的值(value),用于指定并控制程序的行为。

Github上推荐读写“.ini”配置文件的C语言开源代码库 (opens new window) ini配置文件格式 (opens new window)

# vector 和 deque 的差别是?

本质上vector的遍历性能与随机查找性能是要高于deque,只要对生命周期过长类的成员进行清空操作就不会 存在问题。避免不了内存占用过大的风险。 友情提醒,vector使用时一定要注意提前分配空间!!

大部分开发人员都是用 vector 替代deque。

# POD 类型有什么优点?

  1. 字节赋值。POD类型变量可以不使用构造函数、赋值操作符赋值,直接通过memset()、memcpy()初始化赋值。
  2. 兼容C内存布局。C++程序可以和C进行交互,或者可以和其他语言交互。
  3. 保证静态初始化安全有效。静态初始化很多时候可以提高程序性能,POD类型初始化更加简单。
typedef <typename T> struct std::is_pod;

cout << is_pod<U>::value <<endl; // 判断是否是 POD 类型。
1
2
3

# 静态初始化和动态初始化的区别是什么?

静态初始化:指的是在编译时期就讲某一些对象进行了初始化;

动态初始化:运行的时候才去进行初始化

# 类类型默认初始值是多少?

对于类类型,其默认初始值由类自己决定:

  • 如果类中的数据成员在默认构造函数中进行了赋值,则默认初始化值优先使用默认构造函数的值
  • 如果在默认构造函数中没有赋值,但是该数据成员提供了类内初始值,则创建对象时,其默认初始值就是类内初始值,
  • 如果在默认构造函数中没有赋值且没有类内初始值,对于内置类型,则其值未定义,对于类类型,则对其进行默认初始化

版权声明:本文为CSDN博主「倒地不起的土豆」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/qq_37766667/article/details/124258726

# delete nullptr 是安全的吗?

C++ 和 C 中,不能直接释放的是野指针,free(NULL) 和 delete nullptr 都是可以的。

所以delete 后将指针指向 nullptr 可以重复 delete 时出错。

# RVO 和 NRVO 是什么?

在C++中,RVO(Return Value Optimization)和NRVO(Named Return Value Optimization)是两种优化技术,用于优化函数返回值的构造和复制操作。 RVO是一种编译器优化技术,它在函数返回时会尝试避免不必要的复制操作,而是直接在函数调用的地方创建对象。简而言之,RVO通过在函数调用栈上分配返回对象的空间,并将该空间用作函数内部局部变量,从而避免了副本的创建和销毁。这样做可以减少内存开销和提高程序的执行效率。 NRVO是RVO的一种特殊情况,它发生在函数内部声明了一个局部对象,并在函数返回时将其作为返回值。NRVO通过直接在函数调用栈上创建返回对象的空间,并将函数内部的局部变量直接构造在该空间中,从而避免了拷贝构造函数的调用。NRVO可以有效地避免不必要的对象复制操作,提高程序的性能。 RVO和NRVO优化都是由编译器自动进行的,无需手动干预。它们在函数返回值为非引用类型时起作用,但并非所有的编译器都会进行RVO和NRVO优化,因此在编写代码时并不能依赖这种优化,而是应该遵循良好的编程习惯,尽量减少不必要的对象复制操作。 现代 C++编译器()强制使用 RVO 和 NRVO 。

RVO 和 NRVO 主要是为了减少临时对象的拷贝构造、析构开销。

# 函数名和数组名的退化

函数名和数组名实际上都不是指针,但是,在使用时可以退化成指针,即编译器可以帮助我们实现自动的转换。 这也可以解释为什么当我们在=号右侧使用函数名时,无论是取值还是取地址都没有问题,因为编译替我们做了相当于强制类型转换的工作,而在当函数名在=号左侧时,右侧的函数指针并没有这个功能,毕竟他们俩不是同一种结构。

# const 实现函数重载

函数重载,const 可以用来进行函数的重载,常对象(实例化时必须用const进行修饰)调用 常成员函数, 普通对象调用 普通成员函数。

# 成员函数可以访问私有成员

C++ 成员函数中访问对象的私有成员问题,有以下几种情况:

  1. C++ 类的成员函数中,允许直接访问该类的对象的私有成员变量。
  2. 类的成员函数中可以直接访问同类型实例的私有变量。
  3. 拷贝构造函数中,可以直接访问另一个同类对象(引用)的私有成员。
  4. 类的成员函数可以直接访问作为其参数的同类型对象的私有成员。

# 引用类型的成员变量初始化

成员变量是引用类型,必须提供构造函数,且和 const 类型一样,必须在初始化列表完成初始化。

# volatile 的误解

volatile 无法确保线程同步,只能抑制编译器的优化,常用于嵌入式单片机编程中。

最后更新: 11/7/2023, 8:36:05 AM