21 class,struct,union
在c++中struct和class的唯一区别是成员的默认访问权限。struct默认是public,而class默认是private。就是为了向下兼容C语言所以在c++中保留了struct
struct 中默认的访问级别是 public,默认的继承级别也是 public;class 中默认的访问级别是 private,默认的继承级别也是 private。
当 class 继承 struct 或者 struct 继承 class 时,默认的继承级别取决于 class 或 struct 本身, class(private 继承),struct(public 继承),即取决于子类的默认继承级别。
struct A{};
class B : A{}; // 默认private 继承
struct C : B{}; // 默认public 继承
联合体的大小为其内部所有变量的最大值,按照最大类型的倍数进行分配大小;结构体分配内存的大小遵循内存对齐原则
最大字段的大小 + n(填充大小) 应该等于 union中最大基本类型(int,char,float..以及指针和引用)大小的倍数
typedef union
{
char c[10];
char cc1; // char 1 字节,按该类型的倍数分配大小
} u11;// sizeof(u11) 10 = 10(最大字段大小) + 0 是最大基本类型char大小的倍数
typedef union
{
char c[10];
int i; // int 4 字节,按该类型的倍数分配大小
} u22;// sizeof(u22) 12 = 10(最大字段大小) + 2 是最大基本类型int(4)大小的3倍
typedef union
{
char c[10];
double d; // double 8 字节,按该类型的倍数分配大小
} u33;// sizeof(u33) 16 = 10(最大字段大小) + 6 是最大基本类型double(8)大小的2倍
22 volatile
volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,编译器不会对相应的对象进行优化,即不会将变量从内存缓存到寄存器中,防止多个线程有可能暂时使用寄存器中的值(保证内存的可见性),如果这个变量由别的程序更新了的话,将出现不一致的现象(多线程中)
当多个线程都会用到某一变量,并且该变量的值有可能发生改变时,需要用 volatile 关键字对该变量进行修饰;中断服务程序中访问的变量或并行设备的硬件寄存器的变量,最好用 volatile 关键字修饰。
在C++多线程中,volatile不具有原子性;无法对代码重新排序实施限制。禁止指令重排,也是针对编译器说的,保证缓存一致性。
能干什么:告诉编译器不要在此内存上做任何优化。如果对内存有只写未读的等非常规操作,如
x=10;
x=20;
//编译器会优化为
x=20;
volatile 就是阻止编译器进行此类优化。
volatile 和原子性没什么关系,原子性是指一条或者多条指令,要么都执行成功,要么都执行失败,不存在个别指令执行成功,个别指令执行失败的现象。
const和volatile两者同时修饰一个对象的典型情况,是用于驱动中访问外部设备的只读寄存器。
23 static变量的生命周期
静态局部变量,使得离开该函数的作用域后,该变量不会销毁,返回到主函数中,该变量依然存在(在主函数中可以通过这个局部静态变量的地址访问到正确的这个静态局部变量),从而使程序得到正确的运行结果。但是,该静态局部变量直到程序运行结束后才销毁
全局变量(包括静态全局变量)是最先构造的(编译器就构造),早于main函数,当然,析构函数也是执行的最晚,晚于main函数。
静态局部变量是要等到程序执行到该声明定义的表达式后,才开始执行构造的。当然,析构函数也是早于全局变量的,但是晚于main函数。
24 extern C的作用
当 C++ 程序 需要调用 C 语言编写的函数,C++ 使用链接指示,即 extern “C” 指出任意非 C++ 函数所用的语言。
// 可能出现在 C++ 头文件
extern "C"{
int strcmp(const char*, const char*);
}
c和c++对同一个函数经过编译后生成的函数名是不同的,由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。如果在c++中调用一个使用c语言编写的模块中的某个函数,那么c++是根据c++的名称修饰方式来查找并链接这个函数,那么就会发生链接错误。所以extern “C”就是告诉编译器,使用C编译器的方式查找链接这个函数
25 memmove 和 memcpy
都是C语言中的库函数,在头文件string.h中,作用是从src中拷贝count长度的内存的内容到dst中
void *memcpy(void *dst, const void *src, size_t count);
void *memmove(void *dst, const void *src, size_t count);
void* my_memcpy(void* dst, const void* src, size_t n)
{
char *tmp = (char*)dst;
char *s_src = (char*)src;
// 这么写的能得到正确结果的前提是 源和目的的内存区域无重叠
while(n--) {
*tmp++ = *s_src++;// 从内存左侧一个字节一个字节地将src中的内容拷贝到dest的内存中
}
return dst;
}
// memmove能保证在源和目的的内存区域发生重叠的情况下也能保证dst结果正确(src已经不能用了)
void* my_memmove(void* dst, const void* src, size_t n)
{
char* s_dst;
char* s_src;
s_dst = (char*)dst;
s_src = (char*)src;
if(s_dst>s_src && (s_src+n>s_dst)) { //发生内存覆盖的情形。
s_dst = s_dst+n-1;
s_src = s_src+n-1;// 从内容的尾部开始一一拷贝 能够保证dst内容正确
while(n--) {
*s_dst-- = *s_src--;
}
}else {// 和memcp一样适用于源和目的的内存区域无重叠
while(n--) {
*s_dst++ = *s_src++;
}
}
return dst;
}
26 strcpy的缺陷
#include
strcpy 函数的缺陷:strcpy 函数不检查目的缓冲区的大小边界,而是将源字符串逐一的全部赋值给目的字符串地址起始的一块连续的内存空间,同时加上字符串终止符,会导致其他变量被覆盖。(即不会检查des的空间是否能够存下source的内容,无脑直接一一拷贝,当des的空间不足以存source时,这样的拷贝回导致不属于des的空间也被覆盖影响)
char* strcpy(char* des,const char* source)
{
char* r=des;
assert((des != NULL) && (source != NULL));
while((*r++ = *source++)!='\0');
return des;
}
27 虚函数 纯虚函数
class A{
public:
virtual void fun()=0;
void fun2();// 抽象类可以有普通成员方法
};
纯虚函数在类中声明时,加上 =0;
含有纯虚函数的类称为抽象类(只要含有纯虚函数这个类就是抽象类),类中只有接口,没有具体的实现方法;
继承纯虚函数的子类,如果没有完全实现基类纯虚函数,依然是抽象类,不能实例化对象。
抽象类无法实例化对象但是可以声明抽象类指针,可以声明抽象类的引用
void A::fun() {// 纯虚函数是可以给实现的(也可以不给),无论给不给抽象类都无法实例化,子类必须重新实现纯虚函数才能实例化
cout<<"哈哈"<<endl;
}
class B:public A{
public:
void fun() override;
};
void B::fun() {
A::fun(); // 输出哈哈
cout<<"呵呵"<<endl;
}
int main(){
B b;
b.fun();
return 0;
}
28 如何禁止拷贝
c++11以前
class Uncopyable
{
public:
Uncopyable() {}
~Uncopyable() {}
private:
Uncopyable(const Uncopyable &); // 拷贝构造函数
Uncopyable &operator=(const Uncopyable &); // 赋值
};
class A : private Uncopyable
// 私有继承(公用继承也行啊,父类的私有成员子类一样不能带调用,即无法拷贝构造父类,
// 则子类也无法拷贝构造--构造子类前需要构造父类),Uncopyable,~Uncopyable变为A的私有成员函数
{
};
但其实不管那种继承方式都可以通过在子类成员函数中调用父类的public成员函数间接访问。因此上述做法只能避免直接拷贝,仍然无法避免间接拷贝。
c++11 直接delete默认的拷贝行为
class noncopyable {
protected:
noncopyable() = default;
~noncopyable() = default;
public:
// 直接用delete禁止拷贝行为
noncopyable(const noncopyable&) = delete;
noncopyable& operator=(const noncopyable&) = delete;
};
class A: private noncopyable {
};
29 为什么使用初始化列表能够减少构造函数的开销
内置数据类型(int float double..),复合类型(指针,引用)- 在成员初始化列表和构造函数体内进行,在性能和结果上都是一样的
用户自定义类型(我们写的类 类型)如果使用类初始化列表,直接调用该成员变量对应的构造函数即完成初始化;如果在构造函数中初始化,因为 C++ 规定,对象的成员变量的初始化动作发生在进入构造函数本体之前,如果构造函数有初始化列表则按声明顺序,以初始化列表值初始化对象。没有初始化列表则按定义顺序调用各个类对象的默认构造函数初始化成员对象。
一些必须在初始化列表初始化的变量。
1 没有默认构造函数的类对象(默认构造被禁止) 若没有提供显示初始化式(没在初始化列表写),则编译器隐式使用该类型的默认构造函数,而该类无默认构造,则编译器报错
2 const 成员或引用类型的成员。因为 const 对象或引用类型只能初始化,不能对他们赋值
在有初始化列表的情况下,会先执行初始化列表的初始化再执行函数体
class A
{
private:
int val;
public:
A()
{
cout << "A()" << endl;
}
A(int tmp)
{
val = tmp;
cout << "A(int " << val << ")" << endl;
}
A& operator= (const A& a)
{
if(this==&a)
{
return *this;
}
this->val=a.val;
cout<<"A& operator= (const& A) "<<endl;
return *this;
}
};
class Test2
{
private:
A ex;// A()!!!! 在进入构造函数本体之前,会进行类成员对象的初始化 -- 会调用一次A()
public:
Test2() // 函数体中赋值的方式
{
// 函数体内赋值构造 调用一次A(int tmp) 构造一个匿名对象A(2)
// 再调用一次拷贝赋值函数 ex对象拷贝匿名对象A(2)
ex = A(2);
}
};
class Test1
{
private:
A ex;
public:
// 初始化列表构造 只调用一次A(int tmp)
Test1() : ex(1) // 成员列表初始化方式
{
}
};
小tip 初始化列表的成员初始化顺序
按照声明的顺序初始化的,而不是按照出现在初始化列表中的顺序
如
class CMyClass {
CMyClass(int x, int y);
int m_x;
int m_y;
// 先声明的m_x再声明m_y
};
CMyClass::CMyClass(int x, int y) : m_y(y), m_x(m_y)
// 不按初始化列表的顺序初始化,先声明的m_x再声明m_y,所以先初始化m_x再初始化m_y
{
// 先用m_y初始化m_x,但是m_y还未被初始化是一个不确定的值将报错
};
30 菱形多继承出现的命名冲突和数据冗余问题 虚继承
// 间接基类
class Base1
{
public:
int var1;
};
// 直接基类
class Base2 : public Base1
{
public:
int var2;
};
// 直接基类
class Base3 : public Base1
{
public:
int var3;
};
class Derive : public Base2, public Base3
{
public:
void set_var1(int tmp) { var1 = tmp; } // error: reference to 'var1' is ambiguous. 命名冲突 二义性 无法分辨要的这个var1是Base2->Base1中的var1还是Base3->Base1中的var1
void set_var2(int tmp) { var2 = tmp; }
void set_var3(int tmp) { var3 = tmp; }
void set_var4(int tmp) { var4 = tmp; }
private:
int var4;
};
派生类 继承自Base2,Base3这两个类有共同的祖先类Base1
这样继承将会导致Derive 中存在Base2->Base1中的var1和Base3->Base1中的var1。
再Derive中的var1变量指向不明,使用var1将会产生二义性
解决方法1 明确声明命名空间
void set_var1(int tmp) { Base2::var1 = tmp; }
// 这里声明成员变量来源于类 Base2,当然也可以声明来源于类 Base3
解决方法2 虚继承
// 直接基类
class Base2 : virtual public Base1 // 虚继承
{
public:
int var2;
};
// 直接基类
class Base3 : virtual public Base1 // 虚继承
{
public:
int var3;
};
// 派生类
class Derive : public Base2, public Base3
{
public:
void set_var1(int tmp) { var1 = tmp; }
void set_var2(int tmp) { var2 = tmp; }
void set_var3(int tmp) { var3 = tmp; }
void set_var4(int tmp) { var4 = tmp; }
private:
int var4;
};
虚继承使子类除了继承父类成员作为自己的成员之外,内部还会有一份内存来保存哪些是父类的成员。
当Derive继承Base2和Base3之后,编译器根据虚继承保存的信息(当前类中哪些成员是来自哪个父类),查到Base2和Base3拥有共同的父类的成员,就不会从Base2和Base3中继承这些,而是直接从共同的祖先类中继承成员,也就是说,Derive直接继承base的成员,然后再继承Base2和Base3各自新增的成员。这样,Derive就不会继承两份内存。
https://blog.csdn.net/castle_kao/article/details/71024411
https://blog.csdn.net/HUGOPIGS/article/details/105464600?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1.pc_relevant_paycolumn_v3&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1.pc_relevant_paycolumn_v3&utm_relevant_index=2
如果类虚继承了一个类,那么会额外添加一个虚基类表,并在对象内存中插入一个指针,指向这个虚基类表,虚基类表指针(跟上面简称虚表指针—虚函数表指针做区分!!!),虚基类表中按照对象继承的顺序排列对象的直接虚继承类到虚基类的偏移。注意:对于在对象中存取虚基类的问题,虚基类表仅是Microsoft编译器的解决办法。在其他编译器中,一般采用在虚函数表中放置虚基类的偏移量的方式。(如果类同时虚继承,自己内部又有虚函数则这个类对应的对象中有虚基表指针和虚函数指针,虚函数指针vfptr在对象内存的偏移量为0(即类对象的首地址就是这个虚表指针),虚基表指针vbptr在vfptr地址+4的地方)
Base2,Base3都虚继承了Base1,在Base2,Base3类都有对应的一张虚基表,Base2,Base3对象中都有一个虚基表指针。
虚基表指针指向的表头存的是当前类::vbptr与当前类首地址的偏移量(==-4),接下来存的就是到其到其虚继承的基类Base1对象的偏移量。
Derive 继承自Base2,Base3,自然也就继承了Base2,Base3中的两个虚基表指针
Derive 的内存空间(VS编译器中),按照继承声明顺序
Base2::vbptr
..Base2成员变量
Base3::vbptr
..Base3成员变量
自己的成员变量
Base1的成员变量
tip 在菱形继承中,可能会存在对虚父类的多次初始化问题,为了避免出现该问题,在采用虚继承的时候,直接由最低层次的子类构造函数直接负责虚父类(父类所虚继承自的那个爷爷类)的构造,初始化列表中有写则调用相应的构造函数,没写则调用默认构造函数,Derive 的构造中的顺序时先构造Base1(虚父类),再按声明顺序构造Base2,Base3,再是其他类内部成员初始化,最后才是Derive的构造函数体。所以注意Base2,Base3(直接父类)与虚父类(Base1)之间的继承关系将会影响,子类(Derive)中对虚父类构造函数的访问限制,有可能造成在子类中无法构造虚父类将导致子类构造失败。如果不加virtual的话,在构造函数的顺序中,每个类只负责自己的直接父类的初始化
补充