C++ 热知识
引用
https://zhuanlan.zhihu.com/p/100050970
https://www.sohu.com/a/300755552_120111838
https://blog.csdn.net/Hello_World_213/article/details/125854669
https://blog.csdn.net/AkieMo/article/details/131729691
https://blog.csdn.net/weixin_61857742/article/details/127344922
https://zhuanlan.zhihu.com/p/629281871
https://zhuanlan.zhihu.com/p/158805900
https://blog.csdn.net/h1666186/article/details/132697048
gcc与g++的区别
GCC: GNU Compiler Collection(GUN 编译器集合),它可以编译C、C++、JAVA、Fortran、Pascal、Object-C等语言。
gcc是GCC中的GUN C Compiler(C编译器);g++是GCC中的GUN C++ Compiler(C++编译器)。
gcc和g++的主要区别
- 对于 .c和.cpp文件,gcc分别当做c和cpp文件编译(c和cpp的语法强度是不一样的)
- 对于 .c和.cpp文件,g++则统一当做cpp文件编译
- 使用g++编译文件时,g++会自动链接标准库STL,而gcc不会自动链接STL
- gcc在编译C文件时,可使用的预定义宏是比较少的
- gcc在编译cpp文件时/g++在编译c文件和cpp文件时(这时候gcc和g++调用的都是cpp文件的编译器),会加入一些额外的宏。
- 在用gcc编译c++文件时,为了能够使用STL,需要加参数 –lstdc++ ,但这并不代表 gcc –lstdc++ 和 g++等价,它们的区别不仅仅是这个。
C++面向对象的知识
- 实例化子类时,首先调用父类的构造函数,然后调用子类的构造函数;
- 可以从类的其他成员函数显式地调用构造函数和析构函数;例如:
#include <iostream>
using namespace std;
class Test{
public:
Test() { cout << "Constructor is executed\n"; }
~Test() { cout << "Destructor is executed\n"; }
void show() {
Test();
this->~Test();
}
};
int main(){
Test t;
t.show();
return 0;
}
输出:
由于一个对象调用了析构函数,该对象就不再存在了; 如果为生命周期已结束的对象调用析构函数,则行为难以预测;
3. 构造函数可以是私有的,例如设计模式中经典的单例模式;
4. 可以从构造函数调用私有成员函数;
5. 当定义了参数化构造函数并且没有显式定义默认构造函数时,编译器不会隐式调用默认构造函数。当我们为一个类定义一个或多个非默认构造函数(带参数)时,也应该显式定义一个默认构造函数(不带参数),因为在这种情况下编译器不会提供默认构造函数。但是,这不是必需的,但始终定义默认构造函数被认为是最佳实践。
6. 复制构造函数:复制构造函数是一个成员函数,它使用同一类的另一个对象来初始化一个对象。ClassName (const ClassName &old_obj);
。什么时候调用复制构造函数? 在 C++ 中,可以在以下情况下调用复制构造函数:
- 当类的对象按值返回时。
- 当类的对象通过值作为参数传递(给函数)时。
- 当一个对象是基于同一类的另一个对象构造时。
- 当编译器生成一个临时对象时。
如果我们不定义自己的复制构造函数,C++ 编译器会为每个类创建一个默认的复制构造函数,它会在对象之间进行成员方式的复制。编译器创建的复制构造函数通常可以正常工作。仅当对象具有指针或资源的任何运行时分配(如文件句柄、网络连接等)时,我们才需要定义自己的复制构造函数。
- 拷贝构造函数:创建对象,并利用同类型参数引用初始化成员变量:A(const A& Obj);赋值构造函数:不创建对象,利用重写操作符函数初始化成员变量:B& operator = (const B& Obj)。例如:
#include <stdio.h>
#include <string.h>
class ClassName
{
public:
// 重写默认构造函数
ClassName(){
printf("默认构造函数被调用\n");
m_pcArray = new char[30];//在堆区开辟30个字节的内存空间
char* pChar = "测试字符串内容";
memcpy(m_pcArray, pChar, strlen(pChar)); //使用memcpy对堆区中的内存赋值;
}
// 重写默认析构函数
~ClassName(){
printf("默认析构函数被调用\n");
if(NULL != m_pcArray){
delete m_pcArray;//释放堆区内存
m_pcArray = NULL;//指针置空
}
}
// 重写拷贝构造函数
ClassName(const ClassName& cbcnObj){
printf(">>>拷贝构造函数被调用\n");
/*类成员含指针变量是,建议重写拷贝构造函数,对指针变量赋值*/
this->m_pcArray = new char[30]; //深拷贝,开辟新的内存空间
memcpy(this->m_pcArray, cbcnObj.m_pcArray, strlen(cbcnObj.m_pcArray));
/*
*默认赋值方式
*this->m_pcArray = cbcnObj->m_pcArray;//浅拷贝,共用已存在的内存空间
*/
}
public:
char* m_pcArray; //类成员变量
};
int main(void){
ClassName cnObj; //调用默认构造函数创建对象,但不初始化成员变量
// ClassName copyWays2;
ClassName copyWays1(cnObj); //使用拷贝构造函数创建对象,同时初始化类成员变量
ClassName copyWays2 = cnObj; //使用拷贝构造函数创建对象,同时初始化类成员变量
return 0;
}
//对象cnObj生命周期结束,第一次自动调用析构函数
//对象copyWays1 生命周期结束,第二次自动调用析构函数
//对象copyWays2 生命周期结束,第三次自动调用析构函数
输出:
如果main函数改为:
int main(void){
ClassName cnObj;
ClassName copyWays2;
ClassName copyWays1(cnObj);
copyWays2 = cnObj; // !此时不使用 拷贝构造函数
return 0;
}
此时输出:
对比上面2个执行结果,发现后者少执行了一次拷贝构造函数,而是执行了编译器默认生成的赋值构造函数,我们下面重写一遍赋值构造函数:
#include <stdio.h>
#include <string.h>
class ClassName
{
public:
//重写默认构造函数
ClassName(){
printf("默认构造函数被调用\n");
m_pcArray = new char[30];//在堆区开辟30个字节的内存空间
char* pChar = "测试字符串内容";
memcpy(m_pcArray, pChar, strlen(pChar)); //使用memcpy对堆区中的内存赋值;
}
//重写默认析构函数
~ClassName(){
printf("默认析构函数被调用\n");
if(NULL != m_pcArray){
delete m_pcArray;//释放堆区内存
m_pcArray = NULL;//指针置空
}
}
//重写拷贝构造函数
ClassName(const ClassName& cbcnObj){
printf("拷贝构造函数被调用\n");
/*类成员含指针变量是,建议重写拷贝构造函数,对指针变量赋值*/
this->m_pcArray = new char[30]; //深拷贝,开辟新的内存空间
memcpy(this->m_pcArray, cbcnObj.m_pcArray, strlen(cbcnObj.m_pcArray));
/*
*默认赋值方式
*this->m_pcArray = cbcnObj->m_pcArray;//浅拷贝,共用已存在的内存空间
*/
}
//重写赋值构造函数
ClassName& operator = (const ClassName& dbObj)
{
printf(">>>赋值构造函数被调用\n");
if (this == &dbObj){
return *this;
}
//重写赋值语句
memcpy(this->m_pcArray, dbObj.m_pcArray, strlen(dbObj.m_pcArray));
//默认赋值语句
//this->m_pcArray = cbObj.m_pcArray;
}
public:
char* m_pcArray; //类成员变量
};
int main(void)
{
ClassName cnObj; //调用默认构造函数
ClassName copyCnObj;
copyCnObj = cnObj; //调用赋值构造函数
return 0;
}
//对象cnObj生命周期结束,第一次自动调用析构函数
//对象copyCnObj生命周期结束,第二次自动调用析构函数
输出:
-
编译器生成的默认的拷贝构造和赋值构造函数的传入指针或引用参数时会调用浅拷贝,析构函数会造成内存泄漏(参考: https://blog.csdn.net/qq_42981122/article/details/127163850,以及https://blog.csdn.net/qq_42981122/article/details/127163850),解决方法是:当Class中的数据成员中有指针时,重写拷贝构造函数和赋值构造函数,如上所示,在赋值时,重新new一遍,让每次调用函数时,都创建一片堆内存空间,就不会出现同一片堆空间释放两次的情况了。
-
“拷贝构造函数的参数类型为什么必须是引用?”:拷贝构造函数的调用需要创建一个新对象(见上面提到的第7点)。当拷贝构造函数的参数不是引用类型时,而是传值(by value),会触发另一个拷贝构造函数的调用。这样就形成了无限递归的循环,导致栈溢出或程序崩溃。
printf函数的格式化输出
注意下面代码的输出。printf本质是修饰字符串的%s形式,但是添加了很多限制,首先flags:‘-’代表内容要左对齐;其次,width:‘1’代表格式化字符串的控制宽度,因为低于字符串‘hello’的长度,因此这个限制被忽视;.precision:‘.*’代表精度取决于后面提供的变量d,该精度值规定了最大输出字符数。
int main(){
int c,d;
char strings[] = "hello";
printf("------------------\n");
for(c=4;c>=0;c--){
d = c+1;
printf("|%-1.*s|\n", d, strings);
}
printf("------------------\n");
return 0;
}
输出:
printf
函数格式化的占位符解析,分为flags、width、.precision、length、specifier:
关于const char * p 和 char const * p 和 char * const p 的区别
代码:
#include <iostream>
using namespace std;
namespace t0
{
void test0(){
char str1[] = "Monkey";
char str2[] = "Tiger";
const char * p = NULL;
p = str1;
// p[0] = 'X'; //error: invalid conversion from 'const char*' to 'char' 不能修改p所指向地址的内容
str1[0] = 'X';
while(*p){ cout << *p << " "; p++; }
cout << endl;
p = str2; // 可以修改p指向的目标
while(*p){ cout << *p << " "; p++; }
cout << endl;
}
}
namespace t1
{
void test1(){
char str1[] = "Monkey";
char str2[] = "Tiger";
char const * p = NULL;
p = str1;
// p[0] = 'X'; //error: invalid conversion from 'const char*' to 'char' 不能修改p所指向地址的内容
str1[0] = 'X';
while(*p){ cout << *p << " "; p++; }
cout << endl;
p = str2; // 可以修改p指向的目标
while(*p){ cout << *p << " "; p++; }
cout << endl;
}
}
namespace t2
{
void test2(){
char str1[] = "Monkey";
char str2[] = "Tiger";
char * const p = str1;
p[0] = 'P'; // 可以修改p所指向内存地址的内容
cout << str1 << endl;
// p = str2; // error: assignment of read-only variable 'p' ,p不能指向其他内存地址
}
}
namespace t3
{
void test3(){
char str1[] = "Monkey";
char str2[] = "Tiger";
const char * const p = str1;
// p[0] = 'P'; // error: assignment of read-only location '*(const char*)p'
// 不可以修改p所指向内存地址的内容
str1[0] = 'P';
cout << str1 << endl;
// p = str2; // error: assignment of read-only variable 'p'
// p不能指向其他内存地址
}
}
int main(int argc, char **argv){
// const char * p 和 char const * p 是一个意思 :
t0::test0();
cout << "***********" <<endl;
t1::test1();
cout << "-----------" <<endl;
t2::test2();
cout << "+++++++++++" <<endl;
// 不允许 p 指向地址修改,也不允许修改 p 所指向地址的数据
t3::test3();
return 0;
}
效果:
结论:
- const char * p 和 char const * p 是一个意思,代表不能修改p指针所指向地址的内容,但可以修改p指针所指向的目标地址;
- char * const p 代表不可以修改p指针所指向的目标地址,但可以修改p指针所指向地址的内容;
- const char * const p 代表既不允许 p 指向地址修改,也不允许修改 p 所指向地址的数据;
函数同时有多个返回值
代码:
int x(int a, int b, int c){
int e,g,h;
e = a+b/c; // 12
g = a*b+c; // 29
h = a*b*c; // 120
return (e,g,h);
}
int main(){
int a=12,b=2,c=5,d;
d = x(a,b,c);
printf("%d", d);
return 0;
}
做题遇到的,x函数返回()括起来的三个int整型数居然也不报错,但是实际返回值只有(e,g,h)中的最后一个数h。
C++的内联函数
它的出现是为了代替C语言中的#define宏函数,通常是精简、无递归的代码片段,编译时被直接替换展开内联函数中定义的内容而不需要像函数一样生成堆栈结构,节省处理时间,但是增大了程序的体积,属于“以空间成本换取时间成本”。需要注意一下几点:
- inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体过大,编译器优化时会忽略掉内联。
- 内联函数可以支持安全类型检查,并且能够调试;这两点C语言中的#define宏函数做不到。
- 内联函数不能声明与定义分离,如果分离,必会发生链接错误。因为编译器对于inline的处理,是将inline函数直接展开到调用处,从而inline函数这个函数地址就不需要放在符号表里面了。也就是说,当一个函数有了内联属性,函数地址就不会写入符号表,其他文件调用函数需要通过符号表中的函数地址来匹配从而找到函数定义,这时就找不到了,由于链接不到,结果就会引发链接错误。因此,无地址的inline函数不可以是虚函数。
虚函数
- 构造函数不能是虚函数,析构函数可以是虚函数。
对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。在创建对象时,会先分配内存空间,然后调用对象的构造函数来初始化对象。由于每个对象都有一个虚函数表指针,因此在构造函数初始化列表阶段,会初始化这个指针,指向类的虚函数表。这样,在对象创建完成后,对象就可以正确地调用自己的虚函数了。因此,如果构造函数是虚函数,那么在构造函数初始化列表阶段,虚函数表指针还没有被初始化,这时调用虚函数会导致不可预期的结果,可能会访问到错误的虚函数表或者调用到错误的函数:因此,C++不允许构造函数是虚函数。另一方面,C++虚析构函数的必要性在于,当一个类拥有子类并且该类中包含有动态分配的内存资源时,需要通过虚析构函数来释放这些内存资源。如果不使用虚析构函数,当子类实例被删除时,只会调用基类的析构函数,而不会调用子类的析构函数,从而导致子类中动态分配的内存资源无法正确释放,可能会导致内存泄漏或者程序崩溃。因此,在使用继承和动态内存分配时,为了保证程序的正确性和健壮性,需要使用虚析构函数来释放内存资源。
-
静态成员不可以是虚函数,因为静态成员函数没有this指针,就无法访问到虚函数表。
-
对于多继承:子类有多个虚表(即每个父类都有自己的虚表,看你继承几个父类,就有几张虚表),子类中新的、自定义的虚函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)
-
虚函数表存储在只读数据段(.rodata)、虚函数存储在代码段(.text)、虚表指针的存储的位置与对象存储的位置相同,可能在栈、也可能在堆或数据段等。
-
虚函数的调用需要通过指针或引用进行间接调用,这会增加一定的开销。同时,由于虚函数需要在运行时进行动态绑定,所以会稍微降低程序的执行效率。
-
需要避免在构造函数和析构函数中调用虚函数,因为此时对象还未完全构造或已经被销毁。
虚继承
-
所谓虚继承(virtual)就是子类中只有一份间接父类的数据。该技术用于解决多继承中的父类为非虚基类时出现的数据冗余问题,即菱形继承问题。
而当我们使用虚继承时,结构是上图这样,D中只有一份父类A,当我们调用A中数据时,并不会发生冗余。此时,D对象内部结构是这样:
-
虚继承只有在多继承时才有用。也就是说如果只有一层继承关系或者是单继承都将不起作用。虚继承要求同一个子类的多个父类继承自同一个间接父类。
-
实现原理:在虚继承的类中,会定义一个虚基表指针vbptr,指向虚基表。而虚基表中会存在偏移量,这个量就是表的地址到父类数据地址的距离。
友元函数
- 必须在类的定义中声明友元函数,声明时以关键字friend开头,后跟友元函数的函数原型
- 友元函数的说明可以出现在类的任何地方,包括private、protected和public部分
- 友元不是类的成员,所以友元函数的实现与普通函数一样。在实现时不用“::”指示属于哪个类,只有成员函数才使用“::”作用域符号
- 友元函数不能直接访问类的成员,只能访问对象成员;因此调用友元函数时,在实际参数中需要指出要访问的对象:
#include<iostream>
using namespace std;
class A {
public:
A(int val) :a(val) {}
void fun() {
cout << a << endl; //10
cout << this->a << endl; //10,等价于cout << a << endl;
}
private:
friend void fun1(const A& res);
private:
int a;
};
void fun1(const A& res) { // 访问A类型的res对象的私有成员变量
cout << res.a << endl; //10
}
int main(int argc, char* argv[]) {
A res(10);
res.fun();
fun1(res);
getchar();
return 0;
}
友元类
概念: 友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的保护成员和私有成员。
- 友元关系不能被继承,如果类A是类B的友元,那么类A的所有成员函数都是类B的友元
- 友元关系是单向的,不具有交换性。即类B是类A的友元,则类A不一定是类B的友元,需要看类中是否有相应的声明
- 友元关系不具有传递性。即类B是类A的友元,类C是类B的友元,但类C不一定是类A的友元,需要看类中是否有相应的声明
- 一个类的友元函数或友元类可以访问该类的所有成员
示例如下,类CB是类CA的友元类,CB中的函数是可以直接访问类CA的私有成员的:
倘若没有在类CA中声明友元类CB,则CB是不能直接访问CA的私有成员的:
类的继承
- 类的继承后方法属性变化: private 属性不能够被继承。使用private继承,父类的protected和public属性在子类中变为private;使用protected继承,父类的protected和public属性在子类中变为protected;使用public继承,父类中的protected和public属性不发生改变!
- class类默认的成员属性是private,stuct结构体中是public。