C++关键字

C++关键字

class,struct与union

简述

​ class是我们最为熟悉的C++类声明的关键字,便不再多提了,而C++的struct相比C中struct而言很不一样了,已经扩充了很多东西,而union是一种一种特殊的类,相比前两者就比较少用了,但也不排除有派得上用场的时候。

详解

struct与class

​ 刚从C转自C++时,大多数人总是仍把struct当作原来熟悉的包含各种数据的结构体,其实不然。士别三日,即更刮目相看。struct已经可以包含成员函数,可以继承,甚至可以多态,class能做到的,它基本上都可以做到。

​ 那为啥还要class呢?那是因为它们二者还是有区别的,而且关键是它们的定位是不同的。

​ 首先,struct与class最本质的区别是默认访问控制----struct是public的,class是private的。struct可以继承class,同样class也可以继承struct,但是public继承还是private继承取决于子类而不是基类

​ 简单来说,struct作为数据结构的实现体,它默认的数据访问控制是public的,而class作为对象的实现体,它默认的成员变量访问控制是private的。

​ 其次,“class”这个关键字还用于定义模板参数,就像“typename”。但关键字“struct”不用于定义模板参数。这一点在Stanley B.Lippman写的Inside the C++ Object Model有过说明。

​ 最后,struct仍包含在C中的简单{}赋初值操作,但是这样简单的copy操作,只能发生在简单的数据结构上,而不应该放在对象上。加入一个构造函数或是一个虚函数会使struct更体现出一种对象的特性,而使此{}操作不再有效。事实上,是因为加入这样的函数,使得类的内部结构发生了变化。而加入一个普通的成员函数呢?你会发现{}依旧可用。其实你可以将普通的函数理解成对数据结构的一种算法,这并不打破它数据结构的特性。

​ 总而言之,struct更适合看成是一个数据结构的实现体,class更适合看成是一个对象的实现体

​ ​ 详细代码及学习自:https://www.cnblogs.com/starfire86/p/5367740.html

union与struct

​ C++ union结构式一种特殊的类。它能够包含访问权限、成员变量、成员函数(可以包含构造函数和析构函数)。它不能包含虚函数和静态数据变量。它也不能被用作其他类的基类,它本身也不能有从某个基类派生而来。union中得默认访问权限是public。

​ union类型是共享内存的,以size最大的结构作为自己的大小。每个数据成员在内存中得其实地址是相同的。

​ 即当union的第二个数据成员在赋初值时,会覆盖第一个数据成员的值,存储在同一个地址上。但正因为这个特性,我们可以用它来判断CPU是大端模式还是小端模式,具体实现见下方网址。

​ union与struct不同就在于,struct是以所有数据成员大小总和作为自己的大小,所有数据成员存储在相邻地址上。

详细代码及学习自:https://blog.csdn.net/xiajun07061225/article/details/7295355


private,public与protected

简述

​ 一个类可以派生自多个类,这意味着,它可以从多个基类继承数据和函数。定义一个派生类,我们使用一个类派生列表来指定基类。类派生列表以一个或多个基类命名,形式如下:

	class derived-class: access-specifier base-class

​ 其中,访问修饰符 access-specifier 是 public、protectedprivate 其中的一个,base-class 是之前定义过的某个类的名称。如果未使用访问修饰符 access-specifier,则默认为 private。

​ 这三个关键字有两种情况,一种是在类内部使用,表示访问权限;另外一种是在类的继承层次中访问时,表示继承方式。

详解

​ 根据访问权限总结出不同的访问类型,如下所示:

访问 public protected private
同一个类 yes yes yes
派生类 yes yes no
外部的类 yes no no
友元类 yes yes yes

​ 一个派生类继承了所有的基类方法,但下列情况除外:

  • 基类的构造函数、析构函数和拷贝构造函数。

  • 基类的重载运算符。

  • 基类的友元函数。

    类的成员函数可以调用类中各个属性的成员。而类的对象只能访问类的public属性成员,而不能访问private和protected属性成员。所以,类的对象访问类中private和protected的时候要借助public成员函数,这就是类的接口函数。这种访问机制实现了类的封装。

派生类类成员访问级别设置的原则

1、需要被外界访问的成员直接设置为public

2、只能在当前类中或友元类访问的成员设置为private

3、只能在当前类,子类,友元中访问的成员设置为protected,protected成员的访问权限介于public和private之间。


friend

简述

​ 私有成员只能在类的成员函数内部访问,如果想在别处访问对象的私有成员,只能通过类提供的接口(成员函数)间接地进行。这固然能够带来数据隐藏的好处,利于将来程序的扩充,但也会增加程序书写的麻烦,而友元便是因此产生(ps:原来是为了程序员偷懒)。

​ 友元分为友元类和友元函数。

详解

友元函数

​ 在定义一个类的时候,可以把一些函数(包括全局函数和其他类的成员函数)声明为“友元”,这样那些函数就成为该类的友元函数,在友元函数内部就可以访问该类对象的私有成员了。常用于共享数据或运算符重载。

将全局函数声明为友元的写法如下:

friend  返回值类型  函数名(参数表);

将其他类的成员函数声明为友元的写法如下:

friend  返回值类型  其他类的类名::成员函数名(参数表);

但是,不能把其他类的私有成员函数声明为友元。

友元函数实现代码与普通函数相同(可以在类外或类中,不用加friend和类::)

​ ​ 友元函数虽然能使其他类的成员函数直接访问该类的私有变量,提高效率,但同时它也破坏了封装机制,所以尽量不使用成员函数,除非不得已的情况下才使用友元函数。

友元函数的参数:

因为友元函数没有this指针,则参数要有三种情况:

1、 要访问非static成员时,需要对象做参数;--常用(友元函数常含有参数)

2、 要访问static成员或全局变量时,则不需要对象做参数

3、 如果做参数的对象是全局对象,则不需要对象做参数

友元类

​ 一个类 A 可以将另一个类 B 声明为自己的友元,类 B 的所有成员函数就都可以访问类 A 对象的私有成员。在类定义中声明友元类的写法如下:

	friend  class  类名;

Note:
(1) 友元关系不能被继承。
(2) 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元。
(3) 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元。

详细代码及学习自:https://blog.csdn.net/insistgogo/article/details/6608672


virtual

虚表指针与虚函数表

C++ 类中有虚函数的时候,会有一个存放该类所有虚函数的表,不包括普通函数的函数指针,称为虚函数表;指向该表的指针,称为虚表指针

父类子类共享一个虚表指针,所以不管父子类各有多少虚函数,都只是算成一个虚表指针的占用内存,指向共享的虚函数表,但多继承的话会有多张虚函数表

每个类都会维护一张虚表,编译时,编译器根据类的声明创建出虚表

  • 确定时间:虚函数表在编译的时候就确定了,而类对象的虚函数指针vptr是在运行阶段确定的,这是实现多态的关键!
  • 数据结构:虚函数表实质上是一个指针数组,存放指向虚函数的指针
  • 内存位置:虚函数表存放于常量区(只读),虚函数存放在代码区
  • 虚函数表和类绑定,虚表指针和对象绑定。同一个类的所有对象都使用同一个虚表
class Base 
{
public:
	virtual void a() { cout << "Base::a" << endl; }
 	virtual void b() { cout << "Base::b" << endl; }
 	virtual void c() { cout << "Base::c" << endl; }
 	virtual void d() { cout << "Base::d" << endl; }
 	virtual void e() { cout << "Base::e" << endl; }
};

int main()
{
	Base b;
	//(long*)(&b)=&b
	cout << "虚表指针地址:" << (long*)(&b) << endl;//= &b
	
	//虚表指针值=虚表地址
	cout << "虚表指针值:" << *(long*)(&b) << endl;//10进制
	cout << "虚表地址:" << (long*)*(long*)(&b) << endl;//16进制
	
	//虚表地址再解引用是指向虚表内容,再强转long*地址,最后=虚表第一个虚函数地址
	cout << "虚函数表 — 第一个函数地址:" << (long*)*(long*)*(long*)(&b) << endl;
 
	// 1.第一种函数指针
	typedef void(*pFun)(void);
	//等价于using pFun=void(*)(void);

	//三种取值
	pFun pa = reinterpret_cast<pFun>(*(long*)(((long*)(&b))[0]));
	auto pb = (pFun)((long*)*(long*)(&b))[1];	//虚表地址下标取值
	pFun pc = (pFun) * ((long*)*(long*)(&b) + 2);

	//第二种函数指针
	typedef void(Base::* pFun1)();
	pFun1 pd = *(pFun1*)(*(long*)(&b) + sizeof(pFun1) * 3);


	//第三种函数指针
	typedef void(*pFun2)(Base*);
	pFun2 pe = *(pFun2*)(*(long*)(&b) + sizeof(pFun2) * 4);

	pa();
	pb();
	pc();
	(b.*pd)();
	pe(&b);
 	
}

虚函数重写要求,建议在派生类中的函数头后加入关键字override。如果不一致则会报错

  1. 函数的const属性必须一致。
  2. 函数的返回类型必须相同或协变。
  3. 函数名称与参数列表一致。
  4. 虚函数不能是模板函数。

notice

  • 父类的函数定义时有virtual.子类的函数没有virtual但是其他都相同,也同样构成重写!且子类的子类仍然可以继续重写

  • 父类析构函数是虚函数时,编译器在编译时,析构函数名字统一为destucter,所以只要父类的析构函数是虚函数,那么子类的析构函数钱是否加virtual,都构成重写

  • 析构函数重写后,当需要调用父类析构函数时,通过作用域运算符访问

  • 构造函数中最好不要调用虚函数,因为此时对象很有可能还没有构造出来;析构函数中也一样,因为有可能对象已经释放。

多重继承

B,C继承A,D继承B,C

又称为菱形继承/钻石继承

引发问题:二义性

  1. 解决方法:虚继承
    • 让BC虚继承于A
  2. 作用域限定
  3. 函数覆盖虚基类/虚继承

虚继承与纯虚函数

  • 虚函数:virtual void xx
  • 纯虚函数:virtual void xx()=0
    • 在基类中没有定义,但要求任何派生类都要定义自己的实现方法
  • 虚继承:class B: virtual public A
    • 又名共享继承,各派生类的对象共享基类的的一个拷贝
    • 为了解决多继承时的命名冲突和冗余数据问题,C++提出了虚继承,使得在派生类中只保留一份间接基类的成员。
    • 虚继承会多一个指向虚继承偏移量表指针,先存放自己,再按虚继承顺序存放父类成员域偏移量

抽象类与接口

抽象类

  • C++:带有纯虚函数的类
  • C#:被abstract修饰的类

抽象类不能生成对象,只能用作父类被继承,子类必须实现纯虚函数的具体功能

接口是一种特殊的抽象类,所以也不能生成对象

接口

如果一个类里面只有纯虚函数,没有其他成员函数和数据成员,就是接口类

为何有抽象类,还需接口?
接口用于抽象事物的特性,抽象类用于代码复用。接口带来的最大好处就是避免了多继承带来的复杂性和低效性,并且同时可以提供多重继承的好处。

virtual与static,inline

  • static

    • 静态函数不能成为虚函数,因为静态函数不属于某个类
  • inline

    • inline是编译阶段完成对inline函数的处理,
    • 虚函数如果是确定的对象调用,编译期就确定,那么可以内联
    • 虚函数如果是指针或引用调用,呈现多态性,运行期确定,那么无法内联

const

详解

保证所修饰的内容不被程序所修改

  • const基本用法

    • 声明const对象时必须同时赋值或初始化,类const成员建议使用初始化列表初始化

      const int a = 0;
      int const a = 0; // 等价写法
      
  • const与引用

    • const引用只是确保不去修改引用的内容,与引用的对象是否是const对象无关。

      int a = 0;
      const int &b = a + 1; // 这里a+1生成了一个临时对象
      //等价于
      const int temp = a + 1;
      const int &b = temp;
      
  • const与指针

    • 常量指针:指针所指的地址的值为常量,底层const

      • 若某类型的值为常量,则无法用普通指针指向它
    • 指针常量:指针本身值为常量,即指向的地址不能修改,顶层const

    • 在涉及到多级指针时,可以从右往左阅读声明表达式,确认const修饰的是哪一级。

      • 解引用也是从右往左
      int a=0;
      //----指向常量的指针----const在*前
      const int *c = &a; 
      //等价于
      int const *c = &a;
      //----常量指针----const在*后
      int * const d = &a; 
      // 二者结合:一个指向常量的常量指针
      const int * const e = &a; 
      
      int a = 0;
      int *b = &a; // b是一个一级指针
      int **c = &b; // c是一个二级指针
      int **const *d = &c; // d是一个三级指针
      // d是一个普通三级指针 *d是一个常量二级指针对象 **d 是一个普通一级指针 ***d是一个值
      
  • const与extern

    • 当期望这个const对象存在于全局作用域,可以在声明时加上extern修饰,那么就可以在声明时不赋初值,但必须确保程序至少有一处声明赋初值。

      extern const int num;
      
  • const与函数

    • 修饰函数形参,可保护原数据不被修改,但一般用const引用,兼具效率与安全性
    • 修饰函数的返回值,确保函数的返回值不会被修改
  • const与类

    • 修饰成员变量,变成const成员变量,必须在构造函数的初始化列表中初始化;
    • 修改成员函数,实际修饰隐含的this,变成常成员函数,表示不会修改类内变量
    • 构造函数不能用const修饰(因为const修饰类的成员函数时,该函数不能修改成员变量,但是构造函数要修改类的成员变量,因此不可以由const修饰

const与define

  • 处理方式
    • define是在编译的预处理阶段进行字符串替换
    • const是在编译时确定其值const 定义的常数是变量,也带类型, #define 定义的只是个常数,不带类型
  • 内存空间
    • define:不分配内存,给出的是立即数,有多少次使用就进行多少次替换,在内存中会有多个拷贝,消耗内存大
    • const:在静态存储区中分配空间,在程序运行过程中内存中只有一个拷贝
  • 类型检查
    • define:简单的字符串替换,没有类型检查。
    • const:有数据类型,编译时会进行类型检查
#define N 2+3 //我们预想的N值是5

double a = N/2;  //我们预想的a的值是2.5,可实际上a的值是3.5

define简单实现比较

#define MAX(X, Y) ((X)>(Y)?(X):(Y)) 

#define MIN(X, Y) ((X)<(Y)?(X):(Y))

const与constexpr

const用于为修饰的变量添加只读属性,但仍然可以通过别的变量间接修改;const对象只能调用const成员函数

constexpr关键字用于指明其后是一个常量or常量表达式,因此编译器在编译程序时可以顺带将其结果计算出来,而无需等到程序运行阶段,即可用于声明数组大小


inline

6.1 简述

​ inline 说明符,在用于函数的声明说明符序列时,将函数声明为一个 内联(inline)函数,引入inline的主要原因是用它替代C中复杂易错不易维护的宏函数。

​ 完全在class/struct/union 的定义之内定义的函数,无论它是成员函数还是非成员 friend 函数,均为隐式的内联函数。

inline与define

​ 编译器在编译阶段完成对inline函数的处理,即对inline函数的调用替换为函数的本体。函数定义时,在返回类型前加上关键字inline即把函数指定为内联,函数申明时可加也可不加。

  • 时机:内联函数是在编译时展开,而宏在预处理时展开;在编译的时候,内联函数直接被嵌入到目标代码中去,而宏只是一个简单的文本替换。
  • 编写:内联函数是真正的函数,和普通函数调用的方法一样,在调用点处直接展开,避免了函数的参数压栈操作,减少了调用的开销。而宏定义编写较为复杂,常需要增加一些括号来避免歧义。
  • 类型检查:宏定义只进行文本替换,不会对参数的类型、语句能否正常编译等进行检查。而内联函数是真正的函数,会对参数的类型、函数体内的语句编写是否正确等进行检查。
  • 在类中声明同时定义的成员函数,自动转化为内联函数,因此内联函数可以访问类的成员变量,宏定义则不能。
  • 内联函数在运行时可调试,而宏定义不可以。

缺点

(1)代码膨胀。
inline函数带来的运行效率是典型的以空间换时间的做法。内联是以代码膨胀(复制)为代价,消除函数调用带来的开销。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。

(2)inline函数无法随着函数库升级而升级。
如果f是函数库中的一个inline函数,使用它的用户会将f函数实体编译到他们的程序中。一旦函数库实现者改变f,所有用到f的程序都必须重新编译。如果f是non-inline的,用户程序只需重新连接即可。如果函数库采用的是动态连接,那这一升级的f函数可以不知不觉的被程序使用。

(3)是否内联,程序员不可控。

​ inline函数只是对编译器的建议,是否对函数内联,决定权在于编译器。编译器认为调用某函数的开销相对该函数本身的开销而言微不足道或者不足以为之承担代码膨胀的后果则没必要内联该函数,若函数出现递归,有些编译器则不支持将其内联。

注意事项

1)使用函数指针调用内联函数将会导致内联失败。

2)如果函数体代码过长或者有多重循环语句,if或witch分支语句或递归时,不宜用内联。

3)类的constructors、destructors和虚函数往往不是inline函数的最佳选择。

4)一个inline函数会在多个源文件中被用到,那么必须把它定义在头文件中。

​ 可以将内联理解为C++中对于函数专有的宏,对于C的函数宏的一种改进。对于常量宏,C++提供const替代;而对于函数宏,C++提供的方案则是inline。C++的内联机制,既具备宏代码的效率,又增加了安全性,还可以自由操作类的数据成员,算是一个比较完美的解决方案。

​ 一般来说,内联机制用于优化规模较小,流程直接,频繁调用的函数。

详细代码及学习自:https://blog.csdn.net/K346K346/article/details/52065524


template

模板是C++支持参数化多态的工具,目的就是能够让程序员编写与类型无关的代码;通常有两种形式:函数模板和类模板;

template=class,<>括号中的参数叫模板形参,模板形参不能为空。

template <class T> 
void swap(T& a, T& b)
{
	//...
}
template<class  形参名,class 形参名,…>   
class 类名
{ 
	//...
};
  • 类模板里的静态成员初始化的时候,最前面要加template<>
  • 注意模板编程不支持分离式编译,即模板类/模板函数的声明与定义应该放在头文件里,否则会在链接时报错;
  • 类模板和函数模板都可以被全特化;
  • 重载与特化
    • 重载是实现一个独立的新的函数
    • 特化是不引入新模板实例,只是对原来的主(或者非特化)模板中已经隐式声明的实例提供另一种定义

模板形参

  • 类模板的“<类型参数表>”中可以出现非类型参数:template <class T, int size>
  • 可以为类模板的类型形参提供默认值,但不能为函数模板的类型形参提供默认值。
    • 函数模板和类模板都可以为模板的非类型形参提供默认值。
    • 类模板类型形参默认值和函数的默认参数一样,如果有多个类型形参则从第一个形参设定了默认值之后的所有模板形参都要设定默认值

函数模板与类模板

  • 实例化方式不同:函数模板实例化由编译程序在处理函数调用时自动完成,类模板实例化需要在程序中显示指定。
  • 实例化的结果不同:函数模板实例化后是一个函数,类模板实例化后是一个类。
  • 默认参数:类模板在模板参数列表中可以有默认参数。
  • 特化:函数模板只能全特化;而类模板可以全特化,也可以偏特化。
  • 调用方式不同:函数模板可以隐式调用,也可以显示调用;类模板只能显示调用。

函数查找

在有多个函数和函数模板名字相同的情况下,编译器如下规则处理一条函数调用语句:

  1. 先找参数完全匹配的普通函数(非由模板实例化而得的函数);
  2. 再找参数完全匹配的模板函数;
  3. 再找实参数经过自动类型转换后能够匹配的普通函数;
  4. 上面的都找不到,则报错。

可变参数模板

用省略号表示接受可变数目参数的模板函数或模板类。将可变数目的参数被称为参数包,包括模板参数包和函数参数包。

#include <iostream>

using namespace std;

template <typename T>
void print_fun(const T &t)
{
    cout << t << endl; // 最后一个元素
}

template <typename T, typename... Args>
void print_fun(const T &t, const Args &...args)
{
    cout << t << " ";
    print_fun(args...);
}

int main()
{
    print_fun("Hello", "wolrd", "!");
    return 0;
}
/*运行结果:
Hello wolrd !
*/

全特化与偏特化

对主版本模板类、全特化类、偏特化类的调用优先级从高到低进行排序是:全特化类>偏特化类>主版本模板类。这样的优先级顺序对性能也是最好的。

全特化

  • 有时为了需要,针对特定的类型,需要对模板进行特化,也就是所谓的特殊处理
  • 已经不具有template的意思了,明确遇到特定类型会如何处理
//原
template <class T>
class Compare
{
public:
     bool IsEqual(const T& arg, const T& arg1);
};
//全特化
template <>
class Compare<float>
{
public:
     bool IsEqual(const float& arg, const float& arg1);
};

偏特化

  • 偏特化是指提供另一份template定义式,而其本身仍为templatized
  • 偏特化后仍然带有templatized
// 一般化设计
template <class T, class T1>
class TestClass
{
public:
     TestClass()
     {
          cout<<"T, T1"<<endl;
     }
};

// 针对普通指针的偏特化设计
template <class T, class T1>
class TestClass<T*, T1*>
{
public:
     TestClass()
     {
          cout<<"T*, T1*"<<endl;
     }
};

delete与default

8.1简述

C++11中,将具有合成版本的成员函数(即默认构造函数或拷贝控制成员)定义为 =default 来显式地要求编译器生成合成的版本(即编译器创建的默认xx函数)。

可以通过将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝,即在函数的参数列表后面加上=delete来指出我们希望将它定义为删除的。

8.2 详解

二者不同之处有下:

​ 1.=default当用于类内修饰成员的声明,合成的函数将隐式地声明为内联地;当用于类外定义时,其合成的函数是非内联的。=delete只能用于函数第一次声明的时候,这与声明含义在逻辑上吻合,因为=default直到编译器生成代码时才需要,而编译器需要知道一个函数是删除的以便禁止试图使用它的操作。

​ 2.可以对任何函数指定=delete,但只能对编译器可以合成的默认构造函数或拷贝控制成员使用=default。

​ Note:析构函数不能是删除的成员,否则我们不能销毁该对象及其成员!

​ 关于合成默认构造函数 https://www.cnblogs.com/QG-whz/p/4676481.html


final与override

finaloverride关键字位置:这两者应放在constvolatile等其他关键字后边,但是应该在纯虚标记,也就是"=0"的前边

final

  • 直接用在类上,紧跟着类名,表示这个类禁止任何其他类继承它,无论是public继承还是private继承。
  • 用于虚函数(父类),函数名后,用于禁止重写该虚函数

override

override用于虚函数(子类),放于函数名后;

  • 用于显示声明该子类虚函数要覆写/重写父类虚函数
  • 同时可以帮忙检查是否正确覆写(有时写漏const也就能提醒了)

使用final需要更多的权衡,但是override就尽管大胆地用(只要本意是覆写)

禁止继承解决方法二:借助友元、虚继承和私有构造函数来实现

C++

#include <iostream>
using namespace std;

template <typename T>
class Base{
    friend T;
private:
    Base(){
        cout << "base" << endl;
    }
    ~Base(){}
};

class B:virtual public Base<B>{   //一定注意 必须是虚继承
public:
    B(){
        cout << "B" << endl;
    }
};

class C:public B{
public:
    C(){}     // error: 'Base<T>::Base() [with T = B]' is private within this context
};


int main(){
    B b;  
    return 0;
}

说明:在上述代码中 B 类是不能被继承的类。
具体原因:

  • 虽然 Base 类构造函数和析构函数被声明为私有 private,在 B 类中,由于 B 是 Base 的友元,因此可以访问 Base 类构造函数,从而正常创建 B 类的对象;
  • B 类继承 Base 类采用虚继承的方式,创建 C 类的对象时,C 类的构造函数要负责 Base 类的构造,但是 Base 类的构造函数私有化了,C 类没有权限访问。因此,无法创建 C 类的对象, B 类是不能被继承的类。
  • 注意:在继承体系中,友元关系不能被继承,虽然 C 类继承了 B 类,B 类是 Base 类的友元,但是 C 类和 Base 类没有友元关系。

explicit

10.1 简述

​ explicit(显式的)的作用是“禁止单参数构造函数”被用于自动型别转换,其中比较典型的例子就是容器类型。在这种类型的构造函数中你可以将初始长度作为参数传递给构造函数。

10.2 详解

​ 首先, C++中的explicit关键字用于修饰类构造函数, 它的作用是表明该构造函数是显示的, 而非隐式的, 跟它相对应的另一个关键字是implicit, 意思是隐藏的,类构造函数默认情况下即声明为implicit(隐式).

如果类构造函数参数大于或等于两个时, 是不会产生隐式转换的, 所以explicit关键字也就无效了.

​ 不过,也有一个例外,当除了第一个参数以外的其他参数都有默认值的时候, explicit关键字依然有效, 此时, 当调用构造函数时只传入一个参数, 等效于只有一个参数的类构造函数。

​ 总而言之,explicit用来禁止编译器隐式调用该构造函数,以防止隐式的类型转换而造成不必要的错误,但对显式调用该函数则没有问题。

详细代码及学习自:https://www.cnblogs.com/ymy124/p/3632634.html


extern与static

static

  • 修饰类成员变量
    • 静态成员为所有类对象所共享,不属于某个具体的实例
    • 必须在类外定义
    • 可用类名::静态成员或者对象.静态成员来访问。
  • 修饰类成员函数
    • 类的静态成员函数没有默认的this指针
    • 静态成员函数不可以调用非静态成员函数
    • 非静态的成员函数可以调用静态的成员函数

全局变量:用extern声明,也称之为外部变量,是在方法外部定义的变量。它不属于哪个方法,而是属于整个源程序。作用域是整个源程序。如果全局变量和局部变量重名,则在局部变量作用域内,全局变量被屏蔽,不起作用。编程时候尽量不使用全局变量。

静态变量:用static声明,从面向对象的角度触发,当需要一个数据对象为整类而非某个对象服务,同时有力求不破坏类的封装性,既要求此成员隐藏在类的内部,有要求对外不可见的时候,就可以使用static。

二者比较

  • 内存中存放在同一位置,即静态区/全局区
  • 从作用域来看(区别)
    • 全局变量其他文件可以访问
    • 静态全局变量只能被当前文件中的函数访问。
  • 从生命周期看:都在程序运行时期全程有效

constexpr

简述

constexpr函数是指能用于常量表达式的函数

将函数声明为constexpr能使其返回值在编译期即可使用

详解

​ constexpr函数必须遵守几项约定:函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条return语句:

constexpr int  first(){return 42};
constexptr int second=first();   //正确,second是一个常量表达式

编译器在编译时验证first函数是常量表达式,将对其调用替换成结果值42.

为了在编译过程中随时展开,constexpr函数被隐式地指定为内联函数。

Note:constexpr函数不一定返回常量表达式,若用一个非常量表达式调用它,则返回非常量表达式。

C++11 语法中,constexpr 可以修饰模板函数,但由于模板中类型的不确定性,因此模板函数实例化后的函数是否符合常量表达式函数的要求也是不确定的。

针对这种情况下,C++11 标准规定,如果 constexpr 修饰的模板函数实例化结果不满足常量表达式函数的要求,则 constexpr 会被自动忽略,即该函数就等同于一个普通函数。

inline和constexpr

​ 和其他函数不同,内联函数和constexpr函数可在程序中多次定义,但内联函数或constexpr函数的多个定义必须完全一致,因此二者通常定义在头文件中。

noexcept

C++11引入

当确定函数不会发射异常,那么就给它加上noexcept

int f(int x)throw();	//c++98
int f(int x)noexcept;	//c++11
  • 带有noexcept的函数有更大机会得到优化,即可以生成更好的目标代码
  • noexcept对于移动操作,swap,内存释放函数和析构函数最有价值

Volatile

与const 对应,通常用于建立语言级别的 memory barrier

用于修饰变量,表示变量随时可能变化,每次使用它时系统必须重新从它所在的内存读取数据

可以防止编译器访问优化,可以保证对特殊地址的稳定访问

做到以下两件事

  • 阻止编译器调整操作 volatile 变量的指令顺序
  • 阻止编译器为了提高速度将一个变量缓存到寄存器而不写回

register关键字刚好相反,为了将代码放在寄存器从而加快频繁调用的变量的读取

编译器访问优化:

当编译器短时间内两次访问同一变量时,会把变量从内存装入 CPU 寄存器中,从而直接访问寄存器里的数据加快访问速度

使用场景

  • 中断服务程序中修改的供其它程序检测的变量需要加 volatile
  • 多任务环境下各任务间共享的标志应该加 volatile
  • 存储器映射的硬件寄存器通常也要加 volatile 说明,因为每次对它的读写都可能由不同意义

当两个线程都要用到某一个变量且该变量的值会被改变时,应该用 volatile 声明,否则编译器进行优化,那么两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,这会造成程序的错误执行

用法

volatile可用于修饰基本类型,用户定义类型,且其修饰和const一样会从类传递到成员

const 有常量指针和指针常量的说法,volatile 也有相应的概念

//修饰数据
const char* cpch;
volatile char* vpch;
//修饰指针本身值,即地址
char* const pchc;
char* volatile pchv;

decltype与auto

multable

用于lambda

用于成员变量

typeof与using

typedef

posted @ 2019-11-05 23:35  AMzz  阅读(372)  评论(0编辑  收藏  举报
//字体