来源:Light-City/CPlusPlusThings: C++那些事 (github.com)
- const
- static
- this
- inline
- sizeof
- 函数指针
- 纯虚函数和抽象类
- vptr,vtable
- virtual
- volatile
- assert
- bit field
- extern C
- struct
- union
- explicit
- friend
- using
- ::
- enum
- decltype
- 引用与指针
- 宏
const在*左边,被指物为常量,*右边指针为常量;尽量让函数返回一个常量值
int i = 0; int *const p1 = &i; // 不能改变 p1 的值,这是一个顶层 const int ci = 42; // 不能改变 ci 的值,这是一个顶层 const int *p2 = &ci; // 允许改变 p2 的值,这是一个底层 const int *const p3 = p2; // 靠右的 const 是顶层 const,靠左的是底层 const const int &r = ci; // 所有的引用本身都是顶层 const,因为引用一旦初始化就不能再改为其他对象的引用,这里用于声明引用的 const 都是底层 const
当执行对象的拷贝操作时,常量是顶层const还是底层const的区别明显。其中,顶层 const 不受什么影响。
i = ci; // 正确:拷贝 ci 的值给 i,ci 是一个顶层 const,对此操作无影响。 p2 = p3; // 正确:p2 和 p3 指向的对象相同,p3 顶层 const 的部分不影响。
与此相对的,底层 const 的限制却不能被忽视。当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层 const 资格,或者两个对象的数据类型必须能够转换,一般来说,非常量可以转化为常量,反之不行。
int *p = p3; // 错误:p3 包含底层 const 的定义,而p没有。假设成功,p 就可以改变 p3 指向的对象的值。 p2 = p3; // 正确:p2 和 p3 都是底层 const p2 = &i; // 正确:int* 能够转化为 const int*,这也是形参是底层const的函数形参传递外部非 const 指针的基础。 int &r = ci; // 错误:普通 int& 不能绑定到 int 常量中。 const int &r2 = i; // 正确:const int& 可以绑定到一个普通 int 上。
只有常成员函数才有资格操作常量或常对象、调用常成员函数。对于类中的const成员变量必须通过初始化列表进行初始化,不能在类声明中初始化 const 数据成员。
class Apple{
private:
int people[100];
public:
Apple(int i);
const int apple_number;
};
Apple::Apple(int i):apple_number(i)
{
}
const对象只能访问const成员函数。
要使const变量能够在其他文件中访问,必须在文件中显式地指定它为extern。
//extern_file1.cpp extern const int ext=12; //extern_file2.cpp #includeextern const int ext; int main(){ std::cout< static 变量:声明为static时,空间将在程序的生命周期内分配。多次调用函数,静态变量的空间也只分配一次,前一次调用中的变量值通过下一次函数调用传递,(例:count值累加)。
**类中的静态变量:**由对象共享,静态变量不能使用构造函数初始化。类中的静态变量应由用户使用类外的类名和范围解析运算符显式初始化。
class Apple { public: static int i; Apple(){}; }; int Apple::i = 1; int main() { Apple obj; cout << obj.i; //1 }**对象:**在声明为static时具有范围,直到程序的生命周期。
**类中的静态函数:**建议使用类名和范围解析运算符调用静态成员。允许静态成员函数仅访问静态数据成员或其他静态成员函数,它们无法访问类的非静态数据成员或成员函数。
thisthis作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数.
在类的非静态成员函数中返回类对象本身的时候,直接使用 return *this。
当参数与成员变量名相同时,如this->n = n (不能写成n = n)。
inline内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。
inline是一种“用于实现的关键字,而不是用于声明的关键字”。
类中定义的函数是隐式内联函数。声明要想成为内联函数,必须在实现处(定义处)加inline关键字。inline virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如 base::who())。
sizeof
空类大小为1字节。
类的static成员变量不影响类的大小,被编译器放在程序的一个global data members中。不管这个类产生了多少个实例,还是派生了多少新的类,静态数据成员只有一个实例。静态数据成员,一旦被声明,就已经存在。
对于包含虚函数的类,不管有多少个虚函数,只有一个虚指针,vptr的大小,即8。
普通单继承,继承就是基类+派生类自身的大小(注意字节对齐),类的数据成员按其声明顺序加入内存,与访问权限无关,只看声明顺序。派生类继承基类vptr,8字节。
派生类虚继承多个虚函数,会继承所有虚函数的vptr。
class A { public: char b; static int c; virtual void fun(); }; void A::fun(){} class A1 { public: char a; int b; }; class B1 :A1 { public: short a; long b; }; class C1 { public: A1 a; int b; }; class A2 { void virtual fun(); }; class B2 { void virtual fun2(); }; class C2 :virtual public A2, virtual public B2 { void virtual fun3(); }; int A::c = 2; int main() { A a; cout << sizeof(A) << endl;//16 cout << A::c << endl; cout << sizeof(A1) << endl;//8 cout << sizeof(B1) << endl;//16 cout << sizeof(C1) << endl;//12 cout << sizeof(A2) <<" " << sizeof(B2)<<" " << sizeof(C2)<< endl;//8 8 32 return 0; }函数指针typedef int (*fun_ptr)(int,int); // 声明一个指向同样参数、返回值的函数指针类型// 回调函数 void populate_array(int *array, size_t arraySize, int (*getNextValue)(void)) { for (size_t i=0; i函数指针变量可以作为某个函数的参数来使用的,回调函数就是一个通过函数指针调用的函数。
纯虚函数和抽象类
纯虚函数:没有函数体,声明赋值0(定义为虚函数是为了允许用基类的指针来调用子类的这个函数。定义为纯虚函数才代表函数没有被实现。)
抽象类:包含纯虚函数的类;只能作为基类派生新类,由派生类继承实现,如果派生类不覆盖纯虚函数也会变成抽象类,不能创建对象;成员函数可以调用纯虚函数,但构造函数/析构函数不能使用纯虚函数。
构造函数不能是虚函数,而析构函数可以是虚析构函数。
class base { int x; public: virtual void show() = 0; int gexX() { return 0; } base() { cout << "Constructor: base" << endl; } virtual ~base() { cout << "Destructor : base" << endl; } }; class Derived :public base { public: void show() { cout << "in derived" << endl; } Derived() { cout << "Constructor: Derived" << endl; } ~Derived() { cout << "Destructor : Derived" << endl; } }; int main() { //base b; //error: An abstract class cannot be instantiated base *bp = new Derived();//基类指针指向派生类对象 bp->show(); delete bp; return 0; }vptr,vtableC++的动态多态性是通过虚函数来实现的。简单的说,通过virtual函数,指向子类的基类指针可以调用子类的函数。每个使用虚函数的类(或者从使用虚函数的类派生)都有自己的虚拟表。该表只是编译器在编译时设置的静态数组。虚拟表中的每个条目只是一个函数指针,指向该类可访问的派生函数。编译器还会添加一个隐藏指向基类的指针,我们称之为vptr。vptr在创建类实例时自动设置,以便指向该类的虚拟表。
base *pt = new Derived(); // 基类指针指向派生类实例 cout<<"基类指针指向派生类实例并调用虚函数"<fun1(); 其过程为:首先程序识别出fun1()是个虚函数,其次程序使用pt->vptr来获取Derived的虚拟表。第三,它查找Derived虚拟表中调用哪个版本的fun1()。这里就可以发现调用的是Derived::fun1()。因此pt->fun1()被解析为Derived::fun1()!
virtualvolatile
虚函数的调用取决于指向或者引用的对象的类型,而不是指针或者引用自身的类型。基类指针指向继承类对象,则调用继承类对象的函数。
默认参数是静态绑定的,虚函数是动态绑定的。 默认参数的使用需要看指针或者引用本身的类型,而不是对象的类型。
静态函数不可以声明为虚函数,同时也不能被const 和 volatile关键字修饰。static成员函数不属于任何类对象或类实例,所以即使给此函数加上virutal也是没有任何意义。虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,静态成员函数没有this指针,所以无法访问vptr。
**构造函数不可以声明为虚函数。同时除了inline|explicit之外,构造函数不允许使用其它任何关键字。**尽管虚函数表vtable是在编译阶段就已经建立的,但指向虚函数表的指针vptr是在运行阶段实例化对象时才产生的。 如果类含有虚函数,编译器会在构造函数中添加代码来创建vptr。如果构造函数是虚的,那么它需要vptr来访问vtable。
析构函数可以声明为虚函数。如果我们需要删除一个指向派生类的基类指针时,应该把基类析构函数声明为虚函数。
虚函数为私有函数时,int main()必须声明为基类的友元,否则编译失败。
虚函数可以是内联函数,内联(编译器)是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
assert
将变量i加上volatile修饰,则编译器保证对变量i的读写操作都不会被优化,从而保证了变量i被外部程序更改后能及时在原程序中得到感知。
当多个线程共享某一个变量时,变量采用 volatile 声明,防止编译器优化把变量从内存装入CPU寄存器中,当一个线程更改变量后,未及时同步到其它线程中导致程序出错。
volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素(操作系统、硬件、其它线程等)更改。所以使用 volatile 告诉编译器不应对这样的对象进行优化。
volatile 关键字声明的变量,每次访问时都必须从内存中取出值(没有被 volatile 修饰的变量,可能由于编译器的优化,从 CPU 寄存器中取值)
const 可以是 volatile (如只读的状态寄存器)
指针可以是 volatile
断言,是宏,而不是函数,检查逻辑上不可能的情况。如果它的条件返回错误,则终止程序执行。在引用头文件之前#define NDEBUG禁用断言。
#include#include int main() { int x = 7; x = 9; // Programmer assumes x to be 7 in rest of the code assert(x==7); return 0; } 输出:
assert: assert.c:13: main: Assertion 'x==7' failed.bit field数据以位的形式紧凑的储存,并允许程序员对此结构的位进行操作,不可移植。
//struct bit_field_name //{ // type member_name : width; //}; struct stuff { unsigned int field1: 30; unsigned int : 2;//填充作用 unsigned int field2: 4; unsigned int : 0; unsigned int field3: 3; };//该结构现在大小为 3 * 32 = 96 Bits一个位域成员不允许跨越两个 unsigned int 的边界,编辑器会自动移位位域成员使其按照 unsigned int 的边界对齐。也可以使用一个宽度为 0 的未命名位域成员令下一位域成员与下一个整数对齐。
extern C用于C++链接在C语言模块中定义的函数
- C++调用C函数:
//xx.h extern int add(...) //xx.c int add(){ } //xx.cpp extern "C" { #include "xx.h" }
- C调用C++函数
//xx.h extern "C"{ int add(); } //xx.cpp int add(){ } //xx.c extern int add();不过与C++调用C接口不同,C++确实是能够调用编译好的C函数,而这里C调用C++,不过是把C++代码当成C代码编译后调用而已。也就是说,C并不能直接调用C++库函数。
structstruct 更适合看成是一个数据结构的实现体,class 更适合看成是一个对象的实现体。
区别:
默认的继承访问权限。struct 是 public 的,class 是 private 的。
struct 作为数据结构的实现体,它默认的数据访问控制是 public 的,而 class 作为对象的实现体,它默认的成员变量访问控制是 private 的。
unionunion 可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当某个成员被赋值后其他成员变为未定义状态。联合有如下特点:
- 默认访问控制符为 public
- 可以含有构造函数、析构函数
- 不能含有引用类型的成员
- 不能继承自其他类,不能作为基类
- 不能含有虚函数
- 匿名 union 在定义所在作用域可直接访问 union 成员
- 匿名 union 不能包含 protected 成员或 private 成员
union UnionTest { UnionTest() :i(10) { print(i); } ~UnionTest() {}; int i; int j; private: void print(int i) { cout << i << endl; } }; static union {//全局匿名联合必须是静态的 int i; double d; }; int main() { ::i = 20; std::cout << ::i << std::endl; // 输出全局静态匿名联合的 20 UnionTest t; t.i = 5; printf("now t.i is %ld! the address is %pn", t.i, &t.i);//5 addr1 t.i = 6; printf("now t.j is %ld! the address is %pn", t.j, &t.j);//6 addr1 printf("now t.i is %ld! the address is %pn", t.i, &t.i);//6 addr1 return 0; }explicit
- explicit 修饰构造函数时,可以防止隐式转换和复制初始化
- explicit 修饰转换函数时,可以防止隐式转换,但按语境转换除外
class Test1 { public : Test1(int num):n(num){} private: int n; }; class Test2 { public : explicit Test2(int num):n(num){} private: int n; }; int main() { Test1 t1 = 12; Test2 t2(13); Test2 t3 = 14;//error return 0; }friend在类声明的任何区域中声明,而定义则在类的外部。友元函数只是一个普通函数,并不是该类的类成员函数,友元函数中通过对象名来访问该类的私有或保护成员。
友元类的声明在该类的声明中,而实现在该类外。类B是类A的友元,那么类B可以直接访问A的私有成员。
- 友元关系没有继承性
假如类B是类A的友元,类C继承于类A,那么友元类B是没办法直接访问类C的私有或保护成员。- 友元关系没有传递性
假如类B是类A的友元,类C是类B的友元,那么友元类C是没办法直接访问类A的私有或保护成员,也就是不存在“友元的友元”这种关系。class A { public: A(int _a) :a(_a) {}; friend int getA(A&aa); friend class B; private: int a; }; class B { public: int getA(A ca) { return ca.a; } }; int getA(A &aa) { return aa.a; } int main() { A a(3); cout << getA(a) << endl;//3 B b; cout << b.getA(a) << endl;//3 return 0; }usingnamespace ns1 { void func(){cout<<"ns1::func"<
改变访问性:在派生类的内部通过using声明语句 , 我们可以忽略继承方式 ,改变派生类中可访问的基类成员在派生类中的访问权限 。
**函数重载:**在继承过程中,派生类可以覆盖重载函数的0个或多个实例,一旦定义了一个重载版本,那么其他的重载版本都会变为不可见。在派生类中使用using声明语句指定一个名字而不指定形参列表,可以把该函数的所有重载实例添加到派生类的作用域中。
对应typedef A B,使用using B=A可以进行同样的操作。
typedef vector::V1; using V2 = vector ;
- 全局作用域符(::name):用于类型名称(类、类成员、成员函数、变量等)前,表示作用域为全局命名空间
- 类作用域符(class::name):用于表示指定类型的作用域范围是具体某个类的
- 命名空间作用域符(namespace::name):用于表示指定类型的作用域范围是具体某个命名空间的
int count=0; // 全局(::)的count class A { public: static int count; // 类A的count (A::count) }; // 静态变量必须在此处定义 int A::count; int main() { ::count=1; // 设置全局的count为1 A::count=5; // 设置类A的count为2 cout<enumC++11 标准中引入了“枚举类”(enum class)。
新的enum的作用域不在是全局的
不能隐式转换成其他类型
可以指定用特定的类型来存储enum
enum class Color3:char; // 前向声明 // 定义 enum class Color3:char { RED='r', BLUE //‘s' }; char c3 = static_cast(Color3::RED); 类中的枚举常量,某些常量只在类中有效:
class Person{ public: typedef enum { BOY = 0, GIRL }SexType; }; //访问的时候通过,Person::BOY或者Person::GIRL来进行访问。枚举常量不会占用对象的存储空间,它们在编译时被全部求值。
枚举常量的缺点是:它的隐含数据类型是整数,其最大值有限,且不能表示浮点。
decltypedecltype“查询”表达式的类型,并不会对表达式进行“求值”,语法是:
decltype (expression)
- 推导表达式类型
int i = 4; decltype(i) a; //推导结果为int。a的类型为int
- 定义类型
using ptrdiff_t = decltype((int*)0 - (int*)0); using nullptr_t = decltype(nullptr); vectorvec; typedef decltype(vec.begin()) vectype; for (vectype i = vec.begin; i != vec.end(); i++)...
- 重用匿名类型
struct { int d ; double b; }anon_s; decltype(anon_s) as ;//定义了一个上面匿名的结构体
- 泛型编程中结合auto,用于追踪函数的返回值类型
templateauto multiply(T x, T y)->decltype(x*y) { return x*y; } 类型推导规则:
如果 exp 是一个不被括号( )包围的表达式,或者是一个类成员访问表达式,或者是一个单独的变量,那么 decltype(exp) 的类型就和 exp 一致,这是最普遍最常见的情况。
如果 exp 是函数调用,那么 decltype(exp) 的类型就和函数返回值的类型一致。
如果 exp 是一个左值,或者被括号( )包围,那么 decltype(exp) 的类型就是 exp 的引用;假设 exp 的类型为 T,那么 decltype(exp) 的类型就是 T&。
**左值和右值:**左值是指那些在表达式执行结束后依然存在的数据,也就是持久性的数据;右值是指那些在表达式执行结束后不再存在的数据,也就是临时性的数据。对表达式取地址,如果编译器不报错就为左值,否则为右值。
引用与指针定义引用的时候必须为其指定一个初始值,但是指针却不需要。引用不能更换目标,指针可以随时改变指向。
int &r; //不合法,没有初始化引用 int *p; //合法,但p为野指针,使用需要小心使用const reference参数作为只读形参,既可以避免参数拷贝还可以获得与传值参数一样的调用方式。
void test(const vector&data) {...} 底层实现时C++编译器实现这两种操作的方法完全相同。
宏预处理(预编译)工作也叫做宏展开:将宏名替换为字符串。
#define <宏名> <字符串> #define PI 3.1415926 #define <宏名> (<参数表>) <宏体> #define A(x) xdo{…}while(0)的使用
#define fun() f1();f2(); if(a>0) fun()这个宏被展开后就是:
if(a>0) f1(); f2();本意是a>0执行f1 f2,而实际是f2每次都会执行,所以就错误了。
为了解决这种问题,在写代码的时候,通常可以采用{}块。
#define fun() {f1();f2();} if(a>0) fun();但是会发现上述宏展开后多了一个分号,实际语法不太对。使用do-while的用法,让接口容易被正确使用,而不易被误用。
#define DOSOMETHING() do{ foo1(); foo2(); }while(0)



