接下来我将持续更新“深度解读《深度探索C++对象模型》”系列,敬请期待,欢迎关注!也可以关注公众号:iShare爱分享,自动获得推文。

写作不易,请有心人到我的公众号上点点赞支持一下,增加一下热度,也好让更多的人能看到,公众号里有完整的文章列表可供阅读。

有以下三种情况,一个类对象的初始化是以同一类型的另一个对象为初值。

第一种情况,定义一个类对象时以另一个对象作为初始值,如下:

class Foo {};
Foo a;
Foo b = a;

第二种情况,当调用一个函数时,这个函数的参数要求传入一个类对象:

class Foo {};
void Bar(Foo obj) {}
Foo a;
Bar(a);

第三种情况,是函数里返回一个类的对象:

class Foo {};
Foo Bar() {
Foo x;
// ...
return x;
}

这几种情况都是用一个类对象做为另一个对象的初值,假如这个类中有定义了拷贝构造函数,那么这时就会调用这个类的拷贝构造函数。但是如果类中没有定义拷贝构造函数,那么又会是怎样?很多人可能会认为编译器会生成一个拷贝构造函数来拷贝其中的内容,那么事实是否如此呢?

C++标准里描述到,如果一个类没有定义拷贝构造函数,那么编译器就会隐式地声明一个拷贝构造函数,它会判断这个拷贝构造函数是nontrivial(有用的、不平凡的)还是trivial(无用的、平凡的),只有nontrivial的才会显式地生成出来。那么怎么判断是trivial还是nontrivial的呢?编译器是根据这个类是否展现出有逐位/逐成员拷贝的语意,那什么是有逐位/逐成员拷贝的语意?来看看下面的例子。

有逐位拷贝语意的情形

#include <string.h>
#include <stdio.h> class String {
public:
String(const char* s) {
if (s) {
len = strlen(s);
str = new char[len + 1];
memcpy(str, s, len);
str[len] = '\0';
}
}
void print() {
printf("str=%s, len=%d\n", str, len);
}
private:
char* str;
int len;
}; int main() {
String a("hello");
a.print();
String b = a;
b.print(); return 0;
}

在上面代码中,是否需要为String类生成一个显式的拷贝构造函数,以便在第25行代码构造对象b时调用它?我们可以来看看上面代码生成的汇编代码,节选main函数部分:

main:								# @main
push rbp
mov rbp, rsp
sub rsp, 48
mov dword ptr [rbp - 4], 0
lea rdi, [rbp - 24]
lea rsi, [rip + .L.str]
call String::String(char const*) [base object constructor]
lea rdi, [rbp - 24]
call String::print()
mov rax, qword ptr [rbp - 24]
mov qword ptr [rbp - 40], rax
mov rax, qword ptr [rbp - 16]
mov qword ptr [rbp - 32], rax
lea rdi, [rbp - 40]
call String::print()
xor eax, eax
add rsp, 48
pop rbp
ret

从汇编代码中看到,除了在第8行调用了带一个参数的构造函数String(const char* s)之外,并没有调用其他的拷贝构造函数,当然全部的汇编代码中也没有见到生成的拷贝构造函数。说明这种简单的情形只需要进行逐位拷贝类对象的内容即可,不需要生成一个拷贝构造函数来做这个事情。看看程序运行输出结果:

str=hello, len=5
str=hello, len=5

这两行输出内容是上面代码第24行和第26行调用的输出,说明这时对象a和对象b的内容是一模一样的,也就是说对象b的内容完全拷贝了对象a的内容。简单解释一下上面的汇编代码,第4行是在main函数里开辟了48字节的栈空间,用于存放局部变量a和b,[rbp - 24]是对象a的起始地址,[rbp - 40]是对象b的起始地址。第11、12行就是将对象a的第一个成员先拷贝到rax寄存器,然后再拷贝给对象b的第一个成员。第13、14行就是将对象a的第2个成员(对象a的地址偏移8字节)拷贝到rax,然后再拷贝给对象b的第2个成员(对象b的地址偏移8字节)。

编译器认为这种情形只需要逐成员的拷贝对应的内容即可,不需要生成一个拷贝构造函数来完成,而且生成一个拷贝构造函数然后调用它,效率要比直接拷贝内容更低下,这种在不会产生副作用的情况下,不生成拷贝构造函数是一种更高效的做法。

上面的结果从编译器的角度来看是没有问题的,而且是合理的,它认为只需要逐成员拷贝就足够了。但是从程序的角度来看,它是有问题的。你能否看出问题出在哪里?首先我们在构造函数中申请了内存,所以需要一个析构函数来在对象销毁的时候来释放申请的内存,我们加上析构函数:

~String() {
printf("destructor\n");
delete[] str;
}

加上析构函数之后再运行,发现程序崩溃了。原因在于内存被双重释放了,对象a中的str指针赋值给对象b的str,这时对象a和对象b的str成员都指向同一块内存,在main函数结束后对象a和对象b先后销毁而调用了析构函数,析构函数里释放了这一块内存,所以导致了重复释放内存引起程序崩溃。这就是浅拷贝与深拷贝的问题,编译器只会做它认为正确的事情,而逻辑上是否正确是程序员应该考虑的事情,所以从逻辑上来看是否需要明确写出拷贝构造函数是程序员的责任,但是如果你认为没有必要明确定义一个拷贝构造函数,比如说不需要申请和释放内存,或者其它需要获取和释放资源的情况,只是简单地对成员进行赋值的话,那就没有必要写出一个拷贝构造函数,编译器会在背后为你做这些事情,效率还更高一些。

为了程序的正确性,我们显式地为String类定义了一个拷贝构造函数,加上之后程序运行就正常了:

// 下面代码暂时忽略了对象中str原本已经申请过内存的情况。
String(const String& rhs) {
printf("copy constructor\n");
if (rhs.str && rhs.len != 0) {
len = rhs.len;
str = new char[len + 1];
memcpy(str, rhs.str, len);
str[len] = '\0';
}
}

运行输出如下,说明自定义的拷贝构造函数被调用了:

str=hello, len=5
copy constructor
str=hello, len=5
destructor
destructor

上面举例了具有逐位拷贝语意的情形,那么有哪些情形是不具有逐位拷贝语意的呢?那就是在编译器需要插入代码去做一些事情的时候以及扩展了类的内容的时候,如以下的这些情况:

  1. 类中含有类类型的成员,并且它定义了拷贝构造函数;
  2. 继承的父类中定义了拷贝构造函数;
  3. 类中定义了一个以上的虚函数或者从父类中继承了虚函数;
  4. 继承链上有一个父类是virtual base class。

下面我们按照这几种情况来一一探究。

需要调用类类型成员或者父类的拷贝构造函数的情形

如果一个类里面含有一个或以上的类类型的成员,并且这个成员的类定义中有一个拷贝构造函数;或者一个类继承了父类,父类定义了拷贝构造函数,那么如果这个类没有定义拷贝构造函数的话,编译器就会为它生成一个拷贝构造函数,用来调用类对象成员或者父类的拷贝构造函数,由于这两种情况差不多,所以放在一起分析。

如在上面的代码中,新增一个Object类,类里含有String类型的成员,见下面的代码:

// String类的定义同上

class Object {
public:
Object(): s("default"), num(10) {}
void print() {
s.print();
printf("num=%d\n", num);
}
private:
String s;
int num;
}; // main函数改成如下
int main() {
Object a;
a.print();
Object b = a;
b.print(); return 0;
}

运行结果如下:

str=default, len=7
num=10
copy constructor
str=default, len=7
num=10
destructor
destructor

从结果中可以看出最重要的两点:

  1. String类的拷贝构造函数被调用了;
  2. 对象b的成员num被赋予正确的值。

我们来进一步,首先看一下生成的汇编代码:

main:																		# @main
push rbp
mov rbp, rsp
sub rsp, 80
mov dword ptr [rbp - 4], 0
lea rdi, [rbp - 32]
mov qword ptr [rbp - 80], rdi # 8-byte Spill
call Object::Object() [base object constructor]
mov rdi, qword ptr [rbp - 80] # 8-byte Reload
call Object::print()
jmp .LBB0_1
.LBB0_1:
lea rdi, [rbp - 72]
lea rsi, [rbp - 32]
call Object::Object(Object const&) [base object constructor]
jmp .LBB0_2
.LBB0_2:
lea rdi, [rbp - 72]
call Object::print()
jmp .LBB0_3
.LBB0_3:
# 以下代码省略

上面是节选main函数的部分汇编代码,执行析构函数部分省略掉。第10行对应的是main函数里的第18行a.print();,编译器会把它转换成print(&a),参数就是对象a的地址,也就是[rbp - 80],把它放到rdi寄存器中作为参数,从上面的代码中知道[rbp - 80]其实等于[rbp - 32],[rbp - 32]就是对象a的地址。第15行代码对应的就是Object b = a;这一行的代码,可见它调用了Object::Object(Object const&)这个拷贝构造函数,但C++的代码中我们并没有显式地定义这个函数,这个函数是由编译器自动生成出来的。它有两个参数,第一个参数是对象b的地址,即[rbp - 72],存放在rdi寄存器,第二个参数是对象a的地址,即[rbp - 32],存放在rsi寄存器。编译器会把上面的调用转换成Object::Object(&b, &a);。

接下来看看编译器生成的拷贝构造函数的汇编代码:

Object::Object(Object const&) [base object constructor]:	# @Object::Object(Object const&) [base object constructor]
push rbp
mov rbp, rsp
sub rsp, 32
mov qword ptr [rbp - 8], rdi
mov qword ptr [rbp - 16], rsi
mov rdi, qword ptr [rbp - 8]
mov qword ptr [rbp - 24], rdi # 8-byte Spill
mov rsi, qword ptr [rbp - 16]
call String::String(String const&) [base object constructor]
mov rax, qword ptr [rbp - 24] # 8-byte Reload
mov rcx, qword ptr [rbp - 16]
mov ecx, dword ptr [rcx + 16]
mov dword ptr [rax + 16], ecx
add rsp, 32
pop rbp
ret

第5、6行是把对象b的地址(rdi寄存器)存放到[rbp - 8]中,把对象a的地址(rsi寄存器)存放到[rbp - 16]中。第10行代码就是去调用String类的拷贝构造函数了。第11到14行代码是用对象a中的num成员的值给对象b的num成员赋值,[rbp - 16]是对象a的起始地址,存放到rcx寄存器中,然后再加16字节的偏移量就是num成员的地址,加16字节的偏移量是为了跳过前面的String类型的成员s,它的大小为16字节。rax寄存器存放的是对象b的起始地址,[rax + 16]就是对象b中的num成员的地址。

从这里可以得出一个结论:编译器生成的拷贝构造函数除了会去调用类类型成员的拷贝构造函数之外,还会拷贝其它的数据成员,包括整形数据、指针和数组等等,它和生成的默认构造函数不一样,生成的默认构造函数不会去初始化这些数据成员。

如果类类型成员里没有定义拷贝构造函数,比如把String类中的拷贝构造函数注释掉,这时编译器就不会生成一个拷贝构造函数,因为不需要,这时它会实行逐成员拷贝的方式,若遇到成员是类类型的,则递归地执行逐成员拷贝的操作。

含有虚函数的情形

从前面的文章中我们知道,当一个类定义了一个或以上的虚函数时,或者继承链上的父类中有定义了虚函数的话,那么编译器就会为他们生成虚函数表,并会扩充类对象的内存布局,在类对象的起始位置插入虚函数表指针,以指向虚函数表。这个虚函数表指针很重要,如果没有设置好正确的值,那么将引起调用虚函数的混乱甚至引起程序的崩溃。编译器往类对象插入虚函数表指针将导致这个类不再具有逐成员拷贝的语意,当程序中没有显式定义拷贝构造函数时,编译器需要为它自动生成一个拷贝构造函数,以便在适当的时机设置好这个虚函数表指针的值。我们以下面的例子来分析一下:

#include <stdio.h>

class Base {
public:
virtual void virtual_func() {
printf("virtual function in Base class\n");
}
private:
int b;
}; class Object: public Base {
public:
virtual void virtual_func() {
printf("virtual function in Object class\n");
}
private:
int num;
}; void Foo(Base& obj) {
obj.virtual_func();
} int main() {
Object a;
Object a1 = a;
Base b = a;
Foo(a);
Foo(b); return 0;
}

看下生成的汇编代码,节选main函数部分:

main:									# @main
push rbp
mov rbp, rsp
sub rsp, 64
mov dword ptr [rbp - 4], 0
lea rdi, [rbp - 24]
call Object::Object() [base object constructor]
lea rdi, [rbp - 40]
lea rsi, [rbp - 24]
call Object::Object(Object const&) [base object constructor]
lea rdi, [rbp - 56]
lea rsi, [rbp - 24]
call Base::Base(Base const&) [base object constructor]
lea rdi, [rbp - 24]
call Foo(Base&)
lea rdi, [rbp - 56]
call Foo(Base&)
xor eax, eax
add rsp, 64
pop rbp
ret

上面汇编代码中的第10行对应C++代码中的第27行,这里调用的是Object类的拷贝构造函数,汇编代码中的第13行对应C++代码中的第28行,这里调用的是Base类的拷贝构造函数,这说明了编译器为Object类和Base类都生成了拷贝构造函数。继续分析这两个类的拷贝构造函数的汇编代码:

Object::Object(Object const&) [base object constructor]:	# @Object::Object(Object const&) [base object constructor]
push rbp
mov rbp, rsp
sub rsp, 32
mov qword ptr [rbp - 8], rdi
mov qword ptr [rbp - 16], rsi
mov rdi, qword ptr [rbp - 8]
mov qword ptr [rbp - 24], rdi # 8-byte Spill
mov rsi, qword ptr [rbp - 16]
call Base::Base(Base const&) [base object constructor]
mov rax, qword ptr [rbp - 24] # 8-byte Reload
lea rcx, [rip + vtable for Object]
add rcx, 16
mov qword ptr [rax], rcx
mov rcx, qword ptr [rbp - 16]
mov ecx, dword ptr [rcx + 12]
mov dword ptr [rax + 12], ecx
add rsp, 32
pop rbp
ret
Base::Base(Base const&) [base object constructor]: # @Base::Base(Base const&) [base object constructor]
push rbp
mov rbp, rsp
mov qword ptr [rbp - 8], rdi
mov qword ptr [rbp - 16], rsi
mov rax, qword ptr [rbp - 8]
lea rcx, [rip + vtable for Base]
add rcx, 16
mov qword ptr [rax], rcx
mov rcx, qword ptr [rbp - 16]
mov ecx, dword ptr [rcx + 8]
mov dword ptr [rax + 8], ecx
pop rbp
ret

在Object类的拷贝构造函数里,上面汇编代码的第10行,调用了Base类的拷贝构造函数,这里的意思是先构造Base子类部分,在Base类的拷贝构造函数里,上面汇编代码的第27行到29行,在这里设置了Base类的虚函数表指针,因为这里构造的是Base子类的对象,所以这里设置的是Base类的虚函数表指针。然后返回到Object类的拷贝构造函数,在上面汇编代码的第12行到第14行,这里又重新设置回Object类的虚函数表指针,因为构造完Base子类之后继续构造Object类,需要重设回Object类的虚函数表指针,Base类和Object类的虚函数表是不同的两个表,所以需要为它们对应的对象设置对应的虚函数表指针。

其实同一类型的对象的赋值是可以采用逐成员拷贝的方式来完成的,比如像Object a1 = a;这行代码,因为它们的虚函数表是同一个,直接拷贝对象a的虚函数表指针给a1对象没有任何问题。但是问题出在于使用派生类的对象给父类的对象赋值时,这里会发生切割,把派生类对象中的父类子对象部分拷贝给父类对象,如果没有编译器扩充的部分(这里是虚函数表指针),只是拷贝数据部分是没有问题的,但是如果把派生类的虚函数表指针赋值给父类子对象,这将导致虚函数调用的混乱,本该调用父类的虚函数的,却调用了派生类的虚函数。所以编译器需要重设这个虚函数表指针的值,也就是说这里不能采用逐成员拷贝的手法了,当程序中没有显式地定义拷贝构造函数时编译器就会生成一个,或者在已有的拷贝构造函数中插入代码,来完成重设虚函数表指针这个工作。

再看下C++代码中的这三行代码:

Base b = a;
Foo(a);
Foo(b);

第一行的赋值语句,虽然是使用派生类Object的对象a作为初值,但是调用的却是Base类的拷贝构造函数(见main函数的汇编代码第13行),因为b的类型是Base类。这就保证了只使用了对象a中的Base子对象部分的内容,以及确保设置的虚函数表指针是指向Base类的虚函数表,这样在调用Foo函数时,分别使用对象a和b作为参数,尽管Foo函数的形参使用的是“Base&”,是使用基类的引用类型,但却不会引起调用上的混乱。第二个调用使用b作为参数,它是Base类的对象,调用的是Base类的虚函数,这两行的输出结果是:

virtual function in Object class
virtual function in Base class

继承链上有virtual base class的情形

当一个类的继承链上有一个virtual base class时,virtual base class子对象的布局会重排,内存布局的分析可以参考另一篇文章《C++对象封装后的内存布局》。为使得能支持虚继承的机制,编译器运行时需要知道虚基类的成员位置,所以编译器会在编译时生成一个虚表,这个表格里会记录成员的相对位置,在构造对象时会插入一个指针指向这个表。这使得类失去了逐成员拷贝的语意,如果一个类对象的初始化是以另一个相同类型的对象为初值,那么逐成员拷贝是没有问题的,问题在于如果是以派生类的对象赋值给基类的对象,这时候会发生切割,编译器需要计算好成员的相对位置,以避免访问出现错误,所以编译器需要生成拷贝构造函数来做这样的事情。以下面的代码为例:

#include <stdio.h>

class Grand {
public:
int g = 1;
}; class Base1: virtual public Grand {
int b1 = 2;
}; class Base2: virtual public Grand {
int b2 = 3;
}; class Derived: public Base1, public Base2 {
int d = 4;
}; int main() {
Derived d;
Base2* pb2 = &d;
d.g = 11;
pb2->g = 10;
Base2 b2 = *pb2; return 0;
}

第25行的代码是将派生类Derived类的对象赋值给Base2父类对象,这将会发生切割,将Derived类中的Base2子对象部分拷贝过去,看下对应的汇编代码:

# 节选部分main函数汇编
mov rsi, qword ptr [rbp - 56]
lea rdi, [rbp - 72]
call Base2::Base2(Base2 const&) [complete object constructor]

[rbp - 56]存放的是C++代码里的pb2的值,也就是对象d的地址,存放在rsi寄存器中,[rbp - 72]是对象b2的地址,存放到rdi寄存器中,然后将rsi和rdi寄存器作为参数传递给Base2的拷贝构造函数,然后调用它。继续看下Base2的拷贝构造函数的汇编代码:

Base2::Base2(Base2 const&) [complete object constructor]:	# @Base2::Base2(Base2 const&) [complete object constructor]
push rbp
mov rbp, rsp
mov qword ptr [rbp - 8], rdi
mov qword ptr [rbp - 16], rsi
mov rax, qword ptr [rbp - 8]
mov rcx, qword ptr [rbp - 16]
mov rdx, qword ptr [rcx]
mov rdx, qword ptr [rdx - 24]
mov ecx, dword ptr [rcx + rdx]
mov dword ptr [rax + 12], ecx
lea rcx, [rip + vtable for Base2]
add rcx, 24
mov qword ptr [rax], rcx
mov rcx, qword ptr [rbp - 16]
mov ecx, dword ptr [rcx + 8]
mov dword ptr [rax + 8], ecx
pop rbp
ret

首先将两个参数(分别存放在rdi和rsi寄存器)拷贝到栈空间[rbp - 8]和[rbp - 16]中,第8到11行代码就是将对象d中的Grand子对象的成员拷贝到b2对象中,对象的前8个字节在构造对象的时候已经设置好了虚表的指针,这里将指针指向的内容存放到rdx寄存器中,第9行取得虚基类成员的偏移地址然后存放在rdx寄存器,第10行将对象的首地址加上偏移地址,取得虚基类的成员然后拷贝到ecx寄存器,在第11行代码里拷贝给[rax + 12],即b2对象的起始地址加上12字节的偏移量(8字节的虚表指针加上成员变量b2占4字节),即完成对Grand类中的成员变量g的拷贝。

所以对于有虚基类的情况,将一个派生类的对象赋值给基类对象时,不能采取逐成员拷贝的手法,需要借助虚表来计算出虚基类的成员的相对位置,以获得正确的成员地址,需要生成拷贝构造函数来完成。

抑制合成拷贝构造函数的情况

C++11标准之后新增了delete关键字,它可以指定不允许编译器生成哪些函数,比如我们不允许拷贝一个类对象,那么可以将此类的拷贝构造函数声明为=delete的。例如标准库中的iostream类,它不允许拷贝,防止两个对象同时指向同一块缓存。如果一个类的定义中有一个类类型成员,而此成员的拷贝构造函数声明为=delete的,或者类的父类中声明了拷贝构造函数为=delete的,那么这个类的拷贝构造函数也会被编译器声明为delete的,这个类的对象将不允许被拷贝,如以下的代码:

class Base {
public:
Base() = default;
Base(const Base& rhs) = delete;
}; class Object {
Base b;
}; int main() {
Object d;
Object d1 = d; // 此行编译错误 return 0;
}

上面代码的第13行会引起编译错误,原因就是Object类没有拷贝构造函数,不允许赋值的操作,同样地,拷贝赋值运算符也将被声明为delete的。

总结

  • 拷贝赋值运算符的情况和拷贝构造函数的情况类似,可以采用上述的方法来分析。
  • 当不需要涉及到资源的分配和释放时,不需要显示地定义拷贝构造函数,编译器会为我们做好逐成员拷贝的工作,效率比去调用一个拷贝构造函数要更高效一些。
  • 当你需要为程序定义一个析构函数时,那么肯定也需要定义拷贝构造函数和拷贝赋值运算符,因为当你需要在析构函数中去释放资源的时候,说明在拷贝对象的时候需要为新对象申请新的资源,以避免两个对象同时指向同一块资源。
  • 当你需要为程序定义拷贝构造函数时,那么也同时需要定义拷贝赋值运算符,反之亦然,但是却并不一定需要定义析构函数,比如在构造对象时为此对象生成一个UUID,这时在析构对象时并不需要释放资源。

此篇文章同步发布于我的微信公众号:深度解读《深度探索C++对象模型》之拷贝构造函数

如果您感兴趣这方面的内容,请在微信上搜索公众号iShare爱分享或者微信号iTechShare并关注,以便在内容更新时直接向您推送。

深度解读《深度探索C++对象模型》之拷贝构造函数的更多相关文章

  1. 【深度探索C++对象模型 | 02】构造函数语意学

    默认构造函数的构造操作.拷贝构造函数额构造操作  注意:默认构造函数和拷贝构造函数在必要时的时候由编译器产生出来. 参考资料 关于默认构造函数的几个错误认识(四种情况下,编译器会生成默认构造函数)

  2. 【C++】深度探索C++对象模型读书笔记--构造函数语义学(The Semantics of constructors)(四)

    成员们的初始化队伍(member Initia 有四种情况必须使用member initialization list: 1. 当初始化一个reference member时: 2. 当初始化一个co ...

  3. 《深度探索c++对象模型》chapter2 构造函数语义学

    关于c++,最常听到的一个抱怨是,编译器背着程序员做了太多事情,conversion运算符是最常被引用的一个例子:jerry schwarz,iostream函数库的建筑师,就曾经说过一个故事,他说他 ...

  4. 深度探索C++对象模型

    深度探索C++对象模型 什么是C++对象模型: 语言中直接支持面向对象程序设计的部分. 对于各个支持的底层实现机制. 抽象性与实际性之间找出平衡点, 需要知识, 经验以及许多思考. 导读 这本书是C+ ...

  5. 《深度探索C++对象模型》读书笔记(一)

    前言 今年中下旬就要找工作了,我计划从现在就开始准备一些面试中会问到的基础知识,包括C++.操作系统.计算机网络.算法和数据结构等.C++就先从这本<深度探索C++对象模型>开始.不同于& ...

  6. c++学习书籍推荐《深度探索C++对象模型》下载

    百度云及其他网盘下载地址:点我 百度云及其他网盘下载地址:点我 编辑推荐 如果你是一位C++程序员,渴望对于底层知识获得一个完整的了解,那么这本<深度探索C++对象模型>正适合你 作者简介 ...

  7. 读书笔记《深度探索c++对象模型》 概述

    <深度探索c++对象模型>这本书是我工作一段时间后想更深入了解C++的底层实现知识,如内存布局.模型.内存大小.继承.虚函数表等而阅读的:此外在很多面试或者工作中,对底层的知识的足够了解也 ...

  8. 柔性数组-读《深度探索C++对象模型》有感 (转载)

    最近在看<深度探索C++对象模型>,对于Struct的用法中,发现有一些地方值得我们借鉴的地方,特此和大家分享一下,此间内容包含了网上搜集的一些资料,同时感谢提供这些信息的作者. 原文如下 ...

  9. 柔性数组-读《深度探索C++对象模型》有感

    最近在看<深度探索C++对象模型>,对于Struct的用法中,发现有一些地方值得我们借鉴的地方,特此和大家分享一下,此间内容包含了网上搜集的一些资料,同时感谢提供这些信息的作者. 原文如下 ...

  10. [读书系列] 深度探索C++对象模型 初读

    2012年底-2014年初这段时间主要用C++做手游开发,时隔3年,重新拿起<深度探索C++对象模型>这本书,感觉生疏了很多,如果按前阵子的生疏度来说,现在不借助Visual Studio ...

随机推荐

  1. SSH原理与实践(三)安装和使用

    主页 个人微信公众号:密码应用技术实战 个人博客园首页:https://www.cnblogs.com/informatics/ 引言 在之前SSH原理与实践系列文章中,我们主要讲解了SSH协议的原理 ...

  2. electron-vite 可用,本机软件开发环境搭建

    electron-vite 可用,本机软件开发环境搭建 https://electron-vite.github.io/

  3. git svn 提交代码日志填写规范 BUG NEW DEL CHG TRP gitz 日志z

    git svn 提交代码日志填写规范 BUG NEW DEL CHG TRP gitz 日志z

  4. glibc 2.23 源码分析

    1. 基础知识 1.1 Linux 进程内存布局 Linux 系统在装载 elf 格式的程序文件时,会调用 loader 把可执行文件中的各个段依次载入到从某一地址开始的空间中(载入地址取决于 lin ...

  5. 向TreeView添加自定义信息

    可在 Windows 窗体 TreeView 控件中创建派生节点,或在 ListView 控件中创建派生项. 通过派生可添加任何所需字段,以及添加处理这些字段的自定义方法和构造函数. 此功能的用途之一 ...

  6. Python爬虫实战系列3:今日BBNews编程新闻采集

    一.分析页面 打开今日BBNews网址 https://news.bicido.com ,下拉选择[编程]栏目 1.1.分析请求 F12打开开发者模式,然后点击Network后点击任意一个请求,Ctr ...

  7. [置顶] java.io.IOException: No such file or directory解决方案之权限问题

    先贴出异常信息: java.io.IOException: No such file or directory at java.io.UnixFileSystem.createFileExclusiv ...

  8. C++类的访问权限

    首先明确一个类的用户有三种: 一类用户:类的成员和友元 二类用户:子类的成员及子类的友元 三类用户:外部的用户代码(通过类的对象或指针) 一个类有三种成员 private:只有一类用户可以访问priv ...

  9. 崩溃bug日志总结3

    目录介绍 1.1 OnErrorNotImplementedException[ Can't create handler inside thread that has not called Loop ...

  10. 三维模型3DTile格式轻量化压缩处理重难点分析

    三维模型3DTile格式轻量化压缩处理重难点分析 在对三维模型3DTile格式进行轻量化压缩处理的过程中,存在一些重要而又困难的问题需要解决.以下是几个主要的重难点: 1.压缩率和模型质量之间的平衡: ...