const

  • 修饰变量
    • 则变量不可修改 ,定义时必须初始化,修饰函数参数可以构成重载。
  • 修饰(成员)函数
    • 表示该函数不会修改类对象的数据成员,相当于const this,修饰this,修饰函数本身也可以构成重载。
  • 修饰指针
    • 按const与指针的位置关系,分常量指针和指针常量,常量指针表示指针是一个常量,不可修改,指针常量表示指针指向一个常量。
  1. class A
  2. {
  3. private:
  4. const int a; // const修饰成员变量,常对象成员,只能在初始化列表赋值
  5. public:
  6. A() { }; // 构造函数
  7. A(int x) : a(x) { }; // 初始化列表
  8. // 这里的const可用于对重载函数的区分
  9. int getValue(); // 普通成员函数
  10. int getValue() const; // const修饰成员函数,常成员函数,不得修改当前对象中的任何数据成员的值
  11. // 本质就是修饰了this指针,使它指向一个常量。
  12. };
  13. void function()
  14. {
  15. A b; // 普通对象,可以调用全部成员函数
  16. const A a; // const修饰变量,常对象,只能调用常成员函数、更新常成员变量
  17. const A *p = &a; // 常指针,指针所指对象是常量
  18. const A &q = a; // 常引用,引用对象是常量
  19. char greeting[] = "Hello";
  20. char* p1 = greeting; // 指针变量,指向字符数组变量
  21. const char* p2 = greeting; // 指针变量,指向字符数组常量
  22. char* const p3 = greeting; // 常指针,指向字符数组变量,指针本身是个常量,不可修改,也就是说一直指向greeting
  23. const char* const p4 = greeting; // 常指针,指向字符数组常量,在上面的基础上,再加上所指向greeting也不可修改
  24. }
  25. void function1(const int Var); // 传递过来的参数在函数内不可变
  26. void function2(const char* Var); // 参数指针所指内容为常量,保证Var所指对象在函数内部不会改变
  27. void function3(char* const Var); // 参数指针为常指针,保证var值不会改变
  28. void function4(const int& Var); // 引用参数在函数内为常量,保证var所引用对象在函数内部不会改变
  29. const int function5(); // 返回一个常数
  30. const int* function6(); // 返回一个指向常量的指针变量,使用:const int *p = function6();
  31. int* const function7(); // 返回一个指向变量的常指针,使用:int* const p = function7();

const、非const函数/变量之间的互调,非const可以使用const,const不可以使用非const,很好理解,低安全性的变量可以使用高安全性的变量。
const和指针组合,从右往左解读。

static

  • 控制存储
    • 在程序内存的全局区
  • 控制作用域

    • 只在声明所在作用域内有效。
  • 修饰变量(静态变量)

    • 存储
      • 存储在全局区,具备全局变量的特点,在main函数之前分配空间并初始化,没有显式初始化,则执行默认初始化。
      • 当是成员变量,则所有对象只保存一个该变量。可通过类直接访问。
    • 作用域
      • 在文件内(函数外)声明,则作用域是当前文件。
      • 如果是在函数内声明,则作用域是该函数内。
  • 修饰函数(静态函数)
    • 作用域
      • 只在声明的文件中可见。
      • 当是成员函数,可通过类直接访问。
    • 存储
      • 能够访问的外部变量只能是全局变量。
  1. // 以下代码在fuck.h中
  2. static int a; // 静态变量,执行默认初始化,作用域fuck.h文件
  3. static int a = 1; // 静态变量,初始化只会执行一次,作用域fuck.h文件
  4. int shit()
  5. {
  6. static int count = 1; // 静态变量,函数调用多次,初始化只执行一次,作用域shit函数
  7. }
  8. static int fuck(); // 静态函数,只能访问的外部变量是静态变量。
  9. class A
  10. {
  11. public:
  12. static int a; // 静态成员变量,默认初始化
  13. public:
  14. static void fuck(); // 静态成员函数
  15. }

inline

inline修饰的函数称为内联函数。
原理和宏一样,但是相比于宏,内联函数有函数特性,即有类型检查。多用于修饰一些函数体较为简单的函数。不能包含循环、递归、switch。
类声明中定义的函数都隐式转换成内联函数,虚函数除外。
inline是对编译器的请求,不一定会真正生效,比如对虚函数进行内联,则可能不会生效。

  1. inline int fuck(...); // 声明1(加 inline,建议使用)
  2. int fuck(...); // 声明2(不加 inline)
  3. inline int fuck(...) {/****/};
  4. class A {
  5. int fuck() { return 0; } // 隐式内联
  6. inline virtual fuck_virtual(); // 虚函数内联?内联可能不会生效
  7. }
  8. inline int A::fuck() { return 0; } // 需要显式内联
  9. // 内联发生在编译阶段,而虚函数多态发生在运行时阶段。
  10. // 如果fuck_virtual是以多态的形式调用,在编译阶段无法确定对象类型,也就无法确定调用哪块代码
  11. // 也就不能内联。
  12. inline int A::fuck_virtual() { return 0; }
  13. A a;
  14. a.fuck_virtual(); // 内联生效,因为并没有触发多态,可以确定对象类型
  15. A* a;
  16. a->fuck_virtual(); // 内联不生效,触发多态,不能确定对象类型,。

强制类型转换符

类型转换是有风险的,以前C的转换非常简单,前面加个括号,这种做法不利于分辨类型转换的风险,也不利于问题查找,C决定提供几个不同等级的类型转换,每种转换用于不同场景,如果用错就会编译报错,提醒程序员这个类型转换的风险。
4个类型转换运算符:

  • static_cast
    • 用于风险低的类型转换,这种转换不会出现什么严重问题,一般是一些隐式类型转换,如数值类型间的转换,子类转换成父类(向上转换是隐式转换)
  • dynamic_cast
    • 用于多态类型转换,只适用指针或引用。转换失败就返回nullptr,安全性高。
    • 多态类型转换:子类和父类之间互相转换。
    • 可能会抛出bad_cast异常。
  • const_cast
    • const与非const、volatile与非volatile的类型转换。
  • reinterpret_cast
    • reinterpret的意思是重新解释,即重新解释内存地址中的二进制值,就是告诉编译器这是一个new Type。比如int解释成int,char解释成int,classA解释成classB*。
    • 可以自由使用,但是风险很高,务必清除知道其意义。
    • 一个实际用途是在哈希函数中,即,通过让两个不同的值几乎不以相同的索引结尾的方式将值映射到索引。
    • 不会改变const、volatile

      sizeof和strlen

  • sizeof
    • C++运算符。用于获得变量/对象/类型的字节数。
    • 在编译阶段计算长度。
    • 易搞错情况,sizeoof(数组)获得数组大小,注意是数组,不是数组指针,后者返回的是指针大小。
  • strlen
    • c库函数,在中声明。返回字符串的长度(以\0结尾,不包含\0字符)。
    • 在运行时计算长度。
  1. A a; // 类型A可以是任意类型
  2. A& b = a; // 引用
  3. A* c = &a; // 指针
  4. A array[10]; // 数组
  5. int size = sizeof(A) // A类型的字节数
  6. int size = sizeof(a); // A类型的字节数
  7. int size = sizeof(b); // A类型的字节数
  8. int size = sizeof(c); // 一个指针的字节数,4/8个字节,平台各异。
  9. int size = sizeof(array); // 数组的大小,10 * sizeof(A)
  10. void fuck(A a[], int length)
  11. {
  12. int size = sizeof(a); // 这是指针大小,数组是转成了指针进行参数传递。
  13. }
  1. // strlen源代码
  2. size_t strlen(const char* str){
  3. size_t size_ret = 0;
  4. while(*str++) ++size_ret;
  5. return size_ret;
  6. }

sizeof(类)是计算类对象的大小,考虑因子如下:

  • 考虑内存对齐填充的字节数
  • 考虑非静态成员变量
    • 静态成员变量在全局区,并不在对象内存中。
  • 考虑是否为有虚函数
    • 是否有虚函数,决定了类对象还不会有虚函数表指针。
  • 考虑是否是虚继承
  • 空类大小为1个字节
  1. // 假设在64位编译器中编译,对齐基数为8字节
  2. // A的成员变量首地址必须被这个数整除,min(对齐基数,A中最宽基本类型大小) = 4字节。
  3. class A {
  4. private:
  5. static int s_var; // 不影响类对象大小
  6. const int c_var; // 4字节
  7. int var; // 4字节(上) + 4字节(var) = 8字节
  8. char var1; // 8字节(上)+ 1字节(var1)+ 3字节(填充)= 12字节。
  9. public:
  10. A(int temp) : c_var(temp){} // 成员函数,不影响
  11. ~A(){} // 成员函数,不影响
  12. virtual void f() { cout << "A::f" << endl; } // 有虚函数,则每个类对象都会有一个虚函数表指针
  13. virtual void g() { cout << "A::g" << endl; } // 多个虚函数,也都是只添加一个虚函数表指针
  14. virtual void h() { cout << "A::h" << endl; }
  15. }
  16. int main(){
  17. A a(4);
  18. A *p;
  19. cout << sizeof(p) << endl; // 8字节,指针大小。
  20. cout << sizeof(ex1) << endl; // 24字节
  21. return 0;
  22. }

#pragma pack(n)

设定结构体、联合以及类成员变量以 n 字节方式对齐。

  1. #pragma pack(push) // 保存对齐状态
  2. #pragma pack(4) // 设定为 4 字节对齐
  3. struct test
  4. {
  5. char m1;
  6. double m4;
  7. int m3;
  8. };
  9. #pragma pack(pop) // 恢复对齐状态

explicit

只用于修饰转换构造函数(单参数),声明该构造函数必须显示调用而不能隐式调用,即不能进行隐式转换。
无参和多参构造函数肯定都是显式调用,无所谓显式隐式,修饰是可以修饰,但是没有任何意义。

  1. class A {
  2. public:
  3. explicit A(int a);
  4. }
  5. class B {
  6. public:
  7. explicit B(int a);
  8. }
  9. int main(){
  10. A a(10); // 显式调用
  11. B b(10); // 显式调用
  12. A a = 10; // 错误,已声明为显式调用。
  13. B b = 10; // 隐式调用
  14. }

volatile

告诉编译器,变量的值会被编译器未知的因素(操作系统、硬件、其他线程)修改。防止因编译器的优化导致的错误。比如优化之后变量的值可能缓存到寄存器,而变量内存的值可能被修改,这时候值就是错误的了。因此volatile会请求编译器每次都是从内存中取值。

位域(bitfield)

类的成员是bit为单位(可以1字节以下),可以节省空间。不过它的内存布局是机器相关的。
类可以将其(非静态)数据成员定义为位域(bit-field),在一个位域中含有一定数量的二进制位。当一个程序需要向其他程序或硬件设备传递二进制数据时,通常会用到位域。
C++_固有的不可移植特性

assert

断言,是宏。

  1. // 在C中,定义在<assert.h>中
  2. // 在C++中,定义在<assert>中
  3. #define NDEBUG // 在头文件之前定义,则assert将不生效。
  4. #include <assert>
  5. assert(p != nullptr); // 为false,则立即终止程序

extern “C”

告诉编译器,这块代码用C的方式进行编译链接,就是这段是C代码。

  • extern "C" 修饰的变量和函数是按照 C 语言方式编译和连接的

extern "C" 的作用是让 C++ 编译器将 extern "C" 声明的代码当作 C 语言代码处理,可以避免 C++ 因符号修饰导致代码不能和C语言库中的符号进行链接的问题。

  1. #ifdef __cplusplus
  2. extern "C" {
  3. #endif
  4. void *memset(void *, int, size_t);
  5. #ifdef __cplusplus
  6. }
  7. #endif

using

  1. using std::cin; // using声明,引用命名空间的指定成员
  2. using namespace std; // using指示,引用命名空间的所有成员,尽量少用。
  3. // 尽量少使用using,因为是全部导入,会有多余不用的,另外覆盖了也不会有警告,也难以统计成员使用情况。
  4. class Derived : Base {
  5. public:
  6. using Base::Base; // 在Derived中生成所有和Base中一一对应的构造函数(参数列表相同)
  7. };

#inlcude””与 #inlcude<>

搜索路径的策略不同。
#include<>是在系统配置的库环境中查找
#include””则是先从用户目录开始查找,如果没找到再从系统目录查找。
因此C库文件一般用#include<>,用户自定义文件一般用#include””

#pragma once

  1. // 下面这称为include防范,宏防范,避免重复引入。
  2. #ifndef _FUCK_H_
  3. #define _FUCK_H_
  4. class Fuck {}
  5. #endif
  6. // ****************************************
  7. // 上下的作用相同,显然下面的要更简单。
  8. // ****************************************
  9. // 注意,#pragma once并不是C/C++标准,但是一个广泛被支持的前置处理符号
  10. #pragma once
  11. class Fuck{}

lambda

匿名函数,本质就是一个重载了调用运算法的匿名类。如果是值传递,则类内部有自己的数据成员。
C++_lambda表达式

final

  1. class Base final { // final用途一:阻止继承
  2. public:
  3. virtual void func1() final; // final用途二:阻止虚函数重载
  4. }
  5. class Derived : public Base { // 错误:Base is final class
  6. public:
  7. virtual void func1() { // 错误:func1 is final class
  8. ......
  9. }
  10. }