定义预编译时处理的宏,只是简单的字符串替换,无类型检查,不安全
define定义的一般形式:
#define 标识符 字符串
其中,
#
表示这是一条预处理
命令。凡是以#
开头的均为预处理命令
define
为宏定义命令,
标识符
为所定义的宏名,
字符串
可以是常数、表达式、格式串等。
例如:
#define MAXNUM 99999
C++ 语言允许有参宏定义,在宏定义中的参数称为形参
,在宏调用中的参数称为实参
。
对带参数的宏,在调用中,不仅要宏展开,而且要用实参去代换形参。
带参宏定义一般形式为:
#define 宏名(形参表) 字符串
注意: 在字符串中含有各个形参,在使用时调用带参宏调用的一般形式为:宏名(实参表)
例如:
#define add(x, y) (x + y)
int main()
{
cout << "1 plus 1 is " << add(1, 1.5) << ".\n";
//输出“1 plus 1 is 2.5.”
system("pause");
return(0);
}
这个“函数”定义了加法,但是其没有类型检查,有点类似于模板的感觉,但没有模板安全,可以看作是一个简单的模板。
注意:在其定义的内容(x + y)
加了括号,这里加括号的原因是:宏定义只是在预处理阶段作了简单的替换,如果单纯的替换为 a + b a + b a+b时,当你使用 5 ∗ a d d ( 2 , 3 ) 5 * add(2, 3) 5∗add(2,3)时,其运算过程为 5 ∗ 2 + 3 5 * 2 + 3 5∗2+3,而非我们想要的 5 ∗ ( 2 + 3 ) 5 * (2 + 3) 5∗(2+3)
inline是先将内联函数编译完成,生成了函数体,直接插入被调用的地方,减少了压栈、跳转和返回操作。没有普通函数调用时的额外开销;
内联函数是一种特殊的函数,会进行类型检查;
对编译器的一种请求,编译器有可能会拒绝这种请求;
以实现不同的功能,一般是用于子类在继承父类时,重写父类方法
规则:
1. 重写方法的形参列表,返回值,所抛出的异常与被重写方法一致
2. 被冲洗的方法不能为 `private`
3. 静态方法不能被重写为非静态方法
4. 重写方法的访问修饰符,一定要大于被重写方法的访问修饰符(**public > protected > default > private**)
这些方法的名称相同而参数形式不同
一个方法有不同的版本,存在于一个类中
规则:
1. 不能通过访问权限、返回类型、抛出的的异常进行重载
2. 不同的参数类型可以是不同的参数类型,不同的参数个数,不同的参数顺序(参数类型必须不一样)
3. 方法的异常类型和数目不会对重载造成影响
使用多态,是为了避免在父类里大量重载,引起代码的臃肿且难于维护
重写和重载的本质区别是:加入了override 的修饰符的方法。 此方法始终只有一个被你使用的的方法
new 内存分配失败时,会爆出 bac_alloc 异常,它不会返回NULL; malloc 分配内存失败时,返回NULL。
使用new 操作符申请内存分配时,无需指定内存块的大小,而 malloc 则需要显式地指出所需内存的尺寸。
operator new/operator delete 可以被重载,而malloc/free 不允许重载。
new/delete 会调用对象的构造函数/析构函数,而malloc/free则不会调用。
malloc 与 free 是C++/C 语言标准库函数, new/delete 是C++ 运算符。
new 操作符从自由存储区上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。
new/delete | malloc/free | |
---|---|---|
本质属性 | 运算符 | CRT函数 |
内存分配大小 | 自动计算 | 手工计算 |
类型安全 | 是 (一个int 类型指针指向float会报错) | 不是 (malloc类型转换为int, 分配double数据类型大小的内存空间不会报错) |
两者关系 | new 封装了 malloc | |
其他特点 | 除了分配和释放内存还会调用 构造和析构函数 | 只分配和释放内存 |
内存分配失败会抛出bac_alloc异常 | 内存分配失败会返回NULL | |
返回定义时具体类型的指针 | 返回的是void类型的指针,使用时需要进行类型转换 |
const 表示“只读”的语义,constexpr 表示“常量”的含义
constexpr 只能定义编译期常量,而const 既可以定义编译期常量,也可以定义运行期常量。
复杂系统中很难分辨一个初始值是不是常量表达式,可以将变量声明为constexpr类型,由编译器来验证变量的值是否是一个常量表达式。
constexpr int n = 20;
constexpr int m = n + 1;
static constexpr int MOD = 10000007;
如果constexpr 声明中定义了一个指针,constexpr仅对指针有效,和所指向的对象无关。
constexpr int *p = nullptr; // 常量指针 顶层 const
const int *q = nullptr; // 指向常量的指针,底层const
int *const q = nullptr; // 顶层const
constexpr 函数是指 能用于常量表达式的函数。
函数的返回类型和所有形参类型都是字面值类型,函数体有且只有一条return语句。
constexpr int new() {return 42;}
为了可以在编译过程中展开,constexpr函数被隐式转换为了内联函数。
constexpr 和内联函数 可以在程序中多次定义,一般定义在头文件
构造函数不能说const,但字面值常量类的构造函数 可以是 constexpr。
constexpr 构造函数必须有一个空的函数体,即所有成员变量的初始化都放到初始化列表中。对象调用的成员函数必须使用constexpr修饰。
指针常量: const int *d = new int(2);
常量指针:int *const e = new int(2)
区别方法:
左定值,右定向:指的是const
在*
的左边还是右边
拓展:
顶层const:指针本身是常量;
底层const:指针所指的对象是常量;
若要修改const 修饰的变量的值,需要加上关键字 volatile
若要修改 const 成员函数中某些与类状态无关的数据成员,可以使用mutable
关键字来修饰这个数据成员;
关键字 | 修饰常量【非类中】 | 修饰成员变量 | 修饰成员函数 |
---|---|---|---|
const | 1. 超出其作用域后空间会被释放; 2.在定义时必须初始化,之后无法更改; 3. const 形参可以接受const 和 非const类型的实参; | 只在某个对象的生命周期内是常量,对于整个对象而言是可变的; 不能赋值,不能在类外定义; 只能通过构造函数的参数 初始化列表中初始化 【原因: 因为不同的对象对其const数据成员的值可以不同,所以不能在类中声明时初始化】 | 防止成员函数修改对象的内容【不能修改成员变量的值,但是可以访问】 const对象不可以调用非const 的函数; 但非const 的对象可以调用const 对象。 |
static | 在函数执行后不会释放其内存空间 | 只能用在类定义体 内部的声明,外部初始化,且不加static | ① 作为类作用域的全局函数【不能访问非静态数据成员和调用非静态成员函数】 ② 没有this指针 【不能直接存取 非类的非静态成员,调用非静态成员函数】 ③ 不能声明为virtual |
**const 和 static 不能同时修饰成员函数。 原因:静态成员函数不包含有this指针,即不能实例化;而const成员函数必须具体到某一实例
定义:
作用:
指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值,确保对特殊地址的稳定访问
使用场合:
在中断服务程序和cpu相关寄存器的定义
举例说明:
空循环:
for (volatile int i = 0; i < 10000; i++); // 一定会被执行,不会被优化掉
定义:声明外部变量 【在函数或者文件外部定义的全局变量】
作用:实现多个对象之间的数据共享 + 隐藏,并且使用静态成员还不会破坏隐藏原则;默认初始化为0
self &operator++() {
node = (linktype)((node).next);
return *this;
}
const self operator++(int) {
self tmp = *this;
++*this;
return tmp;
}
为了区分前后置,重载函数是以参数类型来区分,在调用的时候,编译器默默给int指定一个0
1. 为什么后置返回对象,而不是引用
因为后置为了返回旧值创建了一个临时对象,在函数结束的时候这个对象就会被销毁,如果返回引用,该引用毫无意义
2. 为什么后置前面也要加 const
其实也可以不加,但是为了防止你使用i+++++++++
,连续两次调用后置++ 重载符,为什么呢?
原因:
它与内置类型行为不一致;
我们无法获得我们所期望的结果,因为第一次返回值是旧值,而不是原对象,如果连续调用两次 ++ ,结果只累加了一次,所以我们必须手动禁止其合法性,就要在前面加上const。
3. 处理用户的自定义类型
最好使用前置 ++ ,因为他不会创建临时对象,进而不会带来构造和析构而造成的额外开销。
问题: a++
和 int a = b
在C++中是否是线程安全的?
答案:不是
例1:
a++
: 从C/C++ 语法的级别来看,这是一条语句,应该是原子的; 但是从编译器的角度来看,其实不是原子的。
其一般对应三条指令,首先将变量a对应的内存值搬运到某个寄存器(如eax)中,然后将该寄存器的值自增1,再将该寄存器的值搬运回到a代表的内存中。
mov eax, dword ptr [a] # (1)
inc eax # (2)
mov dword ptr [a], eax # (3)
现在假设 i = 0 i = 0 i=0,有两个线程,每个线程对变量a的值都递增1, 预想一下结果应该为2,但是实际运行结果为1,神奇
分析如下:
int a = 0;
// 线程1(执⾏过程对应上⽂汇编指令(1)(2)(3))
void thread_func1() {
a++;
}
// 线程2(执⾏过程对应上⽂汇编指令(4)(5)(6))
void thread_func2() {
a++;
}
我们预想结果是,线程1 和 线程2 的三条指令,各自执行,最终a的值变为2,但是由于操作系统线程调度的不确定性,线程1 执行完指令(1) (2) 后,eax的寄存器中 i 的值变为1,此时操作系统切换到 进程2 执行,执行(3) (4) (5),此时eax的值变为1; 接着操作系统继续执行,执行指令(6), 得到a的最终结果为 1 。
例2:
int a = b;
:从C/C++ 的语法级别看,应该是原子的;但是从编译器的角度来看,由于现在计算机CPU体系的限制,数据不能直接从内存某处搬运到内存的另外一处,必须借助寄存器中转,因此这一条语句一般对应两条计算机指令,即:将变量b的值搬运到寄存器; 将寄存器值搬运到变量a的地址。
mov eax, dword ptr [b]
mov dword prt [a], eax
既然是两条指令,那么多个线程在执行这两条指令时,某个线程可能会在第一条指令执行完毕后被剥夺CPU时间片,切换到另一个线程而出现不确定的情况。
解决办法: C++ 11 新标准发布后改变了这种困境,新标准提出了对整形变量原子操作的相关库,即:std::atomic,这是一个模板类型:
template
struct atomic:
我们可以传入具体的整型类型对模板进行实例化,实际上STL库也提供了这些实例化的模板类型
// 初始化1
std::atomic value;
value = 99;
// 初始化2
// 下面代码在Linux平台上无法编译通过(即 gcc编译器)
std::atomic value = 99;
// 出错的原因在于 这行代码调用的是 std::atomic 的拷贝构造函数
// 而根据C++ 11 语言规范,std::atomic 的拷贝构造函数,使用=delete标记禁止编译器自动生成
// g++在这条规则上 遵循了C++ 11语言规范