纯虚函数与抽象类
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;
- 利用
union
将32bits位域重映射到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
- 指向