《C++那些事》阅读笔记:基础进阶

纯虚函数与抽象类

1. 纯虚函数

  • 没有函数体的虚函数;
  • C++中的纯虚函数/抽象函数是没有实现的虚函数,只需要通过在声明中赋值0来实现纯虚函数。
// 抽象类
class  Test{
    public:
        // 声明虚函数,只需要赋值0即为纯虚函数
        virtual void show() = 0;
};

2. 抽象类

  • 至少包含一个纯虚函数的类;
  • 只能作为基类来派生新类使用,不能创建对象;
  • 可以创建指针和引用,指向由抽象类派生出来的类的对象;
// 利用上面代码的Test抽象类
int main(){
    Test test;  // 错误,不能创建对象
    Test *test1;  // 正确,可以创建抽象类的指针
    Test *test2 = new Test();  // 错误,Test是抽象类,不能创建对象
} 
  • 抽象类中,成员函数内可以调用纯虚函数,构造函数/析构函数不能使用纯虚函数;
  • 一个从抽象类派生而来的,必须实现基类中所有的纯虚函数,才是非抽象类;
// 利用上面代码的Test抽象类
class TestA : public Test{
    void show(){
        cout << "Test的派生类:TestA" << endl;
    }
};

int main(){
    TestA a;
    a.show();
    return 0;
}
  • 构造函数不能是虚函数,但是析构函数可以是虚析构函数,当基类指针指向派生类对象并删除它时,会希望调用合适的析构函数,如果析构函数不是虚函数,则只能调用基类析构函数。

assert

断言

  • 是宏,而非函数,原型定义在<assert.h>(C,C++)中;
  • 作用:如果其条件返回错误,终止程序执行;
  • 需要在源代码开头,即include<assert.h>之前定义 NDEBUG 来关闭assert;
#include <stdio.h>
#include <assert.h>

int main(){
    int x = 7;

    // 程序中间有些大的代码使得x的值意外发生了改变
    x = 9;
    // 程序员判断x的值是否改变,若改变则终止程序
    assert(x == 7);

    return 0;
}
  • 断言主要用于检查逻辑上不可能的情况,可用在代码运行之前或者之后;
  • 断言通常在运行时被禁用。

位域(Bit Field)

1. 位域的性质

  • 定义:一种数据结构,可以把数据以位的形式紧凑的储存,并允许程序员对此结构的位进行操作;
  • 优点:节省空间,对于程序需要成千上万个数据单元时,位域极其适用;
  • 缺点:是不可移植的,实现依赖于机器和系统,不同的平台可能有不同的结果;
  • 位域的类型必须是整型或枚举类型,带符号类型中的位域的行为将因具体实现而定;
  • 取地址运算符(&)不能作用于位域,任何指针都无法指向类的位域。

2. 位域的使用

  • 使用结构体声明,为每个位域成员设置名称和宽度;
// bitFieldName  位域结构名
// type  位域成员的类型,必须为int/signed int/unsigned int类型
// memberName  位域成员名字
// width  成员所占的位数(bits)
struct bitFieldName{
    type memberName : width;
};
  • 声明后可以对位域成员进行赋值,注意值的大小不能超过位与成员的容量,即位数。

3. 位域的大小

  • 例如下面的代码:  
struct box{
    unsigned int a : 1;
    unsigned int   : 3;
    unsigned int b : 4;
}
  • 该结构体中有一个未命名的位域,仅起填充作用,并没有实际意义;
  • 在C语言中使用 unsigned int 作为位域的基本单位,即使一个结构的唯一成员为 1 Bit 的位域,该结构大小也和一个 unsigned int 大小相同
  • 某些系统中,unsigned int 为 16 Bits,在 x86 系统中为 32 Bits。本文中均默认 unsigned int 为 32 Bits

4. 位域的对齐

  • 一个位域成员不能跨越两个unsigned int的边界,若成员的总位数超过了一个unsigned int的大小,则编译器就会自动将位域成员移位,使其按照unsigned int的边界对其;
  • 例如下面的代码:
struct stuff 
{
	unsigned int field1 : 30;
	unsigned int field2 : 4;
	unsigned int field3 : 3;
};
  • 结构中前两个成员的总位数为34bits,超过了32bits,则编译器会将field2移位至下一个unsigned int单元中;
  • field1和field2中有一个2bits的空隙,可以用前面提到过的未命名的位域成员填充,也可以使用一个位数为0的位域成员将下一个位域成员与下一个整数单元对齐。
#include <iostream>

using namespace std;

struct stuff 
{
	unsigned int field1 : 30;
	unsigned int field2 : 4;
	unsigned int field3 : 3;
};
int main(){
    struct stuff s = {1,3,5};
    cout << s.field1 << endl;
    cout << s.field2 << endl;
    cout << s.field3 << endl;
    cout << sizeof(3) <<endl;

    return 0;
}

5. 位域的初始化

  • 直接为结构体赋值:struct stuff s = {1,2,5};
  • 直接为位域成员赋值:s.field1 = 1;

6. 位域的重映射

  • 例如下面的代码:
struct box {
	unsigned int ready : 2;
	unsigned int error : 2;
	unsigned int command : 4;
	unsigned int sector_no : 24;
}b1;
  • 利用重映射将位域归零
// 将“位域结构体的地址”映射到“整形(int *)的地址”
int *p = (int *) &b1;
// 清空,将各成员归零
*p = 0;
  • 利用union32bits位域重映射到unsigned int类型

union的性质:
(1)一种特殊的类,也是一种构造类型的数据结构
(2)一个 union 中可以定义多个不同类型的数据类型,在一个被声明为该 union 的变量中,可以装入 union 所定义的任何一种数据,它们共享同一段内存,节省空间
(3)与 struct 的区别:struct 的总长度为各成员长度之和,union 的长度为成员中最长的长度

  • 利用 union 将位域归零代码如下:
// 涉及到上面的代码
union uBox{
    struct box stBox;
    unsigned int uiBox;
};
union uBox u;
u.uiBox = 0;

C实现C++面向对象特性

1. C++ 实现

  • 多态:在C++中会维护一张虚函数表,其父类的指针或者引用可以指向子类对象;
  • 若一个父类的指针或者引用调用父类的虚函数,则该父类的指针会在自己的虚函数表中查找自己的函数地址;
  • 若一个父类的指针或者引用指向子类的对象,且该子类以近乎重写了父类的虚函数,则该指针会调用子类已经重写的虚函数
  • 代码如下:
#include <iostream>

using namespace std;

// class 默认访问修饰符和继承方式均为 private(私有)
class A{
    public :
        // 虚函数的实现
        virtual void f(){
            cout << "父类 A:f()" << endl;
        }
};
class B : public A{
    public :
        // 虚函数实现,子类中 virtual 关键字可以省略
        virtual void f(){
            cout << "子类 B:f()" << endl;
        }
};

int main(){
    A a;  // 基类对象
    B b;  // 派生对象

    A *pa = &a;  // 父类的指针指向父类对象
    pa -> f();  // 调用父类的函数
    pa = &b;  // 父类指针指向子类对象,多态实现
    pa -> f();  // 调用派生类同名函数

    return 0;
}

2. C 实现

  • 封装:由于 C 中没有 class,考虑使用 struct 来模拟,使用函数指针将属性与方法封装到结构体中;
  • 继承:结构体嵌套
  • 多态:由于 C 中结构体内部没有成员函数,考虑使用函数指针来模拟,但是父子各自的函数指针指向的不是类似C++中维护的虚函数表,而是一块物理内存,不宜模拟过多的函数;
  • 模拟多态,必须要保持函数指针变量对齐,即内容上和变量对齐上完全一致,否则父类指针指向子类对象,会运行崩溃!
  • 代码如下:
// 注意这里使用的语言是 C
#include <stdio.h>

// 重定义一个函数指针类型
typedef void (*pf)();
// 父类
typedef struct _A{
    pf _f;
}A;
// 子类
typedef struct _B{
    // 在子类中定义一个基类的对象即可实现继承
    A _b;
}B;

void funA(){
    printf("%s\n" , "基类 A:fun()");
}
void funB(){
    printf("%s\n" , "子类 B:fun()");
}

int main(){
    A a;
    B b;

    // _f 是一个函数指针
    a._f = funA;  // 父类中的函数指针指向 funA 函数
    b._b._f = funB;  // 子类中的函数指针指向 funB 函数
    A *pa = &a;  // 父类的指针指向父类
    pa -> _f();  // 调用父类函数
    pa = (A *)&b;  // 父类指针指向子类的对象,由于类型不匹配,需要进行强制转换
    pa -> _f();  // 调用子类同名函数

    return 0;
}

const(常类型)

1. const作用

  • 常类型的变量或对象的值是不能被更新的;
  • 定义常量:const int a = 100;
  • const 常量与 #define 宏定义常量的区别:
    • const 常量具有类型,编译器可以安全检查;#define 宏定义是字符串替换,不带类型,不利于安全检查;
    • const 常量生效于编译的阶段,#define 宏定义处于预编译阶段
  • const 定义的变量只有类型为整数或枚举,且以常量表达式初始化时才能作为常量表达式;其他情况只是一个 const 限定的变量,不要与常量混淆;
  • 防止修改,起到保护作用,增强程序健壮性,如下面代码是错误的:
void f(const int i){
    i++;   // 错误!
}
  • 节省空间,避免不必要的内存分配
    • const 定义常量从汇编的角度看,只是给出了对应的内存地址,而 #define 给出的是立即数;
    • const 定义的常量在程序运行过程中只有一份拷贝,???

2. const 对象默认为文件局部变量

  • 非 const 变量默认为extern,要使 const 变量能在其他文件中访问,必须在文件显式地指定它为 extern,且常量在定义时必须要初始化;
//extern_file1.cpp
extern const int ext = 12;

//extern_file2.cpp
#include<iostream>
extern const int ext;
int main(){
    std::cout << ext << std::endl;
}

3. 指针与 const

  • 指向