cpp

virtual function calls 一般而言是通过一个表格 (内含 virtual functions 地址) 的索引而决议得知。

C++ 类设计者的核查表

  • 你的数据成员是私有的吗
  • 你的类需要一个无参的构造函数
  • 是不是每个构造函数初始化所有的数据成员
  • 需要析构函数吗。应该问问自己的构造函数里是否有 new 这样的表达式
  • 需要一个虚析构函数吗。绝不会用作基类的类是不需要虚析构函数的
1
2
3
4
5
6
7
struct B {
String s;
Virtual ~B() {}
}

B* bp = new D;
delete bp;
  • 需要复制构造函数吗。如果你的类在构造函数内分配资源,则可能需要一个显式的复制构造函数来管理资源。下面类可能需要一个复制构造函数:
1
2
3
4
5
6
7
public class {
public:
String();
Stirng(const char* s);
private:
char* data;
}
  • 你的类需要一个赋值操作符吗。如果需要复制构造函数,同理多半也会需要一个赋值操作符。
1
Thing& operator=(const Thing&);
  • 你的赋值操作符能正确地将对象赋给对象本身吗。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 正确实现方式1
String& String::operator=(const String& s) {
if (&s != this) {
delete[] data;
data = new char[strlen(s.data) + 1];
strcpy(data, s.data);
}

return *this;
}

// 正确实现方式2
String& String::operator=(const String& s) {
char* newdata = new char[strlen(s.data) + 1];
strcpy(newdata, s.data);
delete[] data;
data = newdata;
return *this;
}
  • 删除数组时你记住用 delete[] 吗。

C 的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
static int noisy = 1;

void trace(char *s) {
if (noisy)
printf("%s\n", s);
}

void trace_on() {
noisy = 1;
}

void trace_off() {
noisy = 0;
}

函数 trace 不是内联的,即使当跟踪关闭的时候,它还保持着函数调用的开销。在很多 C 的实现中,这个额外负担是无法避免的。

Point 在机器中如何被表示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Point {

public:
Point(float xval);
virtual ~Point();

float x() const;
static int PointCount();

protected:
virtual ostream& print(ostream &os) const;

float _x;
static int _point_count;
}

什么时候使用 struct 什么时候用 class

哪些操作支持多态

  • 隐含的转化:
1
shape *ps = new circle();
  • virtual function 机制:
1
ps->rotate();
  • 经过 dynamic_casttypeid 运算符:
1
if (circle *pc = dynamic_cast<circle *>(ps)) {}

一个 class object 需要多少内存

  • nonstatic data members 的总和大小
  • 加上任何由于 alignment 的需求而填补 (padding) 上去的空间
  • 加上为了支持 virtual 而由内部产生的任何额外负担 (overhead)

什么时候才会合成一个 default constructor


编译器只会在以下 4 种情况,并且 class 自身没有定义 default constructor 的情况下,帮忙合成 default constructor

  • 带有 default constructormember class object: 也就是说,虽然 class A 没有显示定义 default constructor, 但是 A class 内的成员变量 B, B 本身是提供显示default constructor 的,这种情况下会为 A 构造一个 default constructor。在该构造函数里会调用 B 的构造函数,当然只有在被调用的时候才会被合成出来。
  • 带有 default constructorbase class: 也就是说,虽然 class A 没有显示定义 default constructor,但是 A class 继承自 B class, B 本身是提供显示default constructor 的,这种情况下会为 A 构造一个 default constructor.
  • 带有一个 virtual functionclass: 也就是说,虽然 class A 没有显示定义 default constructor,但是 A class有虚函数,既然有虚函数,那么在初始化对象的时候,需要初始化其中的指针 vptr 指向 vtable,所以这种情况下会为 A 构造一个 default constructor.
  • 带有一个 virtual base classclass

同时需要注意,只有再必要的时候编译器才会合成出来,如程序中都没建该 class 的对象,故编译器肯定也不用合成出来。


合成 copy constructor

  • 对一个 object 做明确的初始化操作:
1
2
3
4
5
class X {};
X x;

// 以 x 的内容作为 xx 的初值
X xx = x;
  • 传递参数
1
2
3
4
5
6
extern void foo(X x);
void bar() {
X xx;
// 作为参数传递
foo(xx);
}
  • 返回一个 class object
1
2
3
4
5
X foo_bar() {
X xx;
// 传回一个 class object
return xx;
}

假设 class 设计者明确定义了一个 copy constructor:

1
2
X::X(const X& x);
X::X(const Y& y, int = 0);

那么在大部分情况下,当一个 class object 以另一个同类实体作为初值时,上述的 constructor 会被调用。


1
2
String noun("book");
String verb = noun;

其默认的 copy constructor 完成的操作如下:

1
2
verb.str = noun.str;
verb.len = noun.len;

静态链接库和动态链接库

  • 静态链接库的优点
    (1) 代码装载速度快,执行速度略比动态链接库快;
    (2) 只需保证在开发者的计算机中有正确的.LIB文件,在以二进制形式发布程序时不需考虑在用户的计算机上.LIB文件是否存在及版本问题,可避免DLL地狱等问题。
  • 动态链接库的优点
    (1) 更加节省内存并减少页面交换;
    (2) DLL文件与EXE文件独立,只要输出接口不变(即名称、参数、返回值类型和调用约定不变),更换DLL文件不会对EXE文件造成任何影响,因而极大地提高了可维护性和可扩展性;
    (3) 不同编程语言编写的程序只要按照函数调用约定就可以调用同一个DLL函数;
    (4) 适用于大规模的软件开发,使开发过程独立、耦合度小,便于不同开发者和开发组织之间进行开发和测试。
  • 不足之处
    (1) 使用静态链接生成的可执行文件体积较大,包含相同的公共代码,造成浪费;
    (2) 使用动态链接库的应用程序不是自完备的,它依赖的DLL模块也要存在,如果使用载入时动态链接,程序启动时发现DLL不存在,系统将终止程序并给出错误信息。而使用运行时动态链接,系统不会终止,但由于DLL中的导出函数不可用,程序会加载失败;速度比静态链接慢。当某个模块更新后,如果新模块与旧的模块不兼容,那么那些需要该模块才能运行的软件,统统撕掉。这在早期Windows中很常见。

sizeof

Size of union = size of the largest data type used:

1
2
3
4
5
6
int main() {
union {
char all[13]; // 13
int foo; // 4
} record; // 16
}
1
2
3
4
5
6
7
8
int main() {
#pragma pack(push, 1)
union {
char all[13]; // 13
int foo; // 4
} record; // 13
#pragma pack(pop)
}

Suppose you have this structure:

1
2
3
4
5
6
struct	{
char a[3];
short int b;
long int c;
char d[3];
};

Now, you might think that it ought to be possible to pack this structure into memory like this:

1
2
3
4
5
6
7
+-------+-------+-------+-------+
| a | b |
+-------+-------+-------+-------+
| b | c |
+-------+-------+-------+-------+
| c | d |
+-------+-------+-------+-------+

But it’s much, much easier on the processor if the compiler arranges it like this:

1
2
3
4
5
6
7
8
9
+-------+-------+-------+
| a |
+-------+-------+-------+
| b |
+-------+-------+-------+-------+
| c |
+-------+-------+-------+-------+
| d |
+-------+-------+-------+

In the packed'' version, notice how it's at least a little bit hard for you and me to see how the b and c fields wrap around? In a nutshell, it's hard for the processor, too. Therefore, most compilers willpad’’ the structure (as if with extra, invisible fields) like this:

1
2
3
4
5
6
7
8
9
+-------+-------+-------+-------+
| a | pad1 |
+-------+-------+-------+-------+
| b | pad2 |
+-------+-------+-------+-------+
| c |
+-------+-------+-------+-------+
| d | pad3 |
+-------+-------+-------+-------+

指针

1
2
3
int* pint = 0; // pint指针指向0地址处
pint += 6; // 每次加 1 相当于移动四个字节,值为 24
cout << pint << endl; // 0x18 == 24

queue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <queue>          // std::queue

int main() {
std::queue<int> myqueue;

// Inserts a new element at the end of the queue, after its current last element
myqueue.push(5);

// Returns a reference to the last element in the queue. This is the
// newest element in the queue (the last element pushed into the queue)
myqueue.back();

// Removes the next element in the queue, effectively reducing its size by one.
// The element removed is the oldest element in the queue whose value can be retrieved by calling member queue::front
// This calls the removed element's destructor
myqueue.pop();
}

时间

1
2
3
4
5
6
#include <time.h>

int main() {
// time_t 这种类型就是用来存储从1970年到现在经过了多少秒
time_t now = time(NULL);
}

参考

推荐文章