C++ 基础知识(2)

析构函数的作用

析构函数与构造函数对应,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数。

析构函数名与类名相同,只是在函数名前面加一个位取反符~,例如 ~stud( ),以区别于构造函数。它不能带任何参数,也没有返回值(包括void类型)。只能有一个析构函数,不能重载。

如果用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数(即使自定义了析构函数,编译器也总是会为我们合成一个析构函数,并且如果自定义了析构函数,编译器在执行时会先调用自定义的析构函数再调用合成的析构函数),它也不进行任何操作。所以许多简单的类中没有用显式的析构函数。

如果一个类中有指针,且在使用的过程中动态的申请了内存,那么最好显示构造析构函数在销毁类之前,释放掉申请的内存空间,避免内存泄漏。

类析构顺序:1)派生类本身的析构函数;2)对象成员析构函数;3)基类析构函数。

静态函数和虚函数的区别

静态函数在编译的时候就已经确定运行时机,虚函数在运行的时候动态绑定。虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销。

为什么析构函数必须是虚函数,为什么C++默认的析构函数不是虚函数

将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。

C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。

虚函数表机制

为了实现c++的多态,c++使用了一种动态绑定的机制,其核心是虚函数表。

每个实现了虚函数的类都有一个虚表,虚表是与类绑定的,与对象无关,在程序编译阶段就构造出来了。每个类的虚表只记录虚函数,不记录非虚函数,因此虚函数与非虚函数的调用方式有一点不同。

如果一个子类继承的父类有虚函数,那么该子类也有自己的虚函数表。

为了调用虚函数,每个对象需要保存其类的虚表的起始地址,即虚表指针 *_vptr

执行函数的动态绑定的3个条件:

  1. 通过指针来调用函数
  2. 指针 upcast 向上转型(继承类向基类转换)
  3. 调用的是虚函数

详见c++虚函数表剖析

函数指针

  1. 定义
    函数指针本身首先是一个指针变量,该指针变量指向一个具体的函数。这正如用指针变量可指向整型变量、字符型、数组一样,这里指向函数。c/c++在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型变量一样,在这些概念上是大体一致的。

  2. 用途
    调用函数和做函数的参数,比如回调函数。

  3. 示例

    1
    2
    3
    4
    char *fun(char *p) {…}                        // 函数 fun
    char *(*pf)(char *p); // 函数指针 pf
    pf = fun; // 函数指针 pf 指向函数 fun
    pf(p); // 通过函数指针 pf 调用函数 fun

常量

常量是在程序运行中不能被改变的标识符。
c++中定义常量可以用 #defineconst 这两种方法。

1
2
#define PRICE 10 //定义单价常量10
const int PRICE = 10; //定义单价常量10

区别:
#define是定义宏变量,它其实是在编译之前,由预处理指令把代码里面的宏变量用指定的字符串替换,它不做语法检查,而 constant 则是定义含有变量类型的常量,它会在编译时做语法检查。#define经常被认为好象不是语言本身的一部分。而且有时候用宏,会出现意想不到的输出结果。因此定义常量尽量用编译器而不用预处理。

使用常量的好处:

  1. 增强程序的可读性。用一个有意义的常量字符串代替一个常数,程序读起来会更加的方便。
  2. 如果很多地方用到像PI(3.14159)这样的常量,可以一改改全局。

常量的存放位置

  • 对于局部对象,常量存放在栈区
  • 对于全局对象,常量存放在全局/静态存储区
  • 对于字面值常量,常量存放在常量存储区
    • 在c/c++中,即4, 3.1415926, 0x24, “BEIJING”等

new/delete与malloc/free的区别

new/delete是c++的关键字,而malloc/free是C语言的库函数,new/delete是动态申请,申请的空间大小不需要明确指明,malloc/free是静态申请,必须指明申请内存空间的大小。对于类类型的对象,malloc/free不会调用构造函数和析构函数。

另外,malloc返回的指针需要强转,new会调用构造函数,返回的指针不用强转。

重载和重写/覆盖的区别

重载:两个函数名相同,但是参数列表不同(参数名称、个数、类型不同),返回值类型没有要求,在同一作用域中
重写/覆盖:子类继承父类,父类中的函数是虚函数,在子类中重新定义了这个虚函数,这种情况是重写

strcpy memcpy

1
2
char *strcpy(char* dest, const char *src);                  // 字符串拷贝函数,不指定大小
void *memcpy(void *dest, const void *src, size_t n); // 任意类型复制,指定大小n字节

strcpy()从src逐字节拷贝到dest,直到遇到'\0'结束,因为没有指定长度,可能会导致拷贝越界,造成缓冲区溢出漏洞,安全版本是strncpy函数。
strlen函数是计算字符串长度的函数,返回从开始到'\0'之间的字符个数。

参考链接

C++面试宝典
c++虚函数表剖析
字面值常量