首先, C++中的explicit关键字只能用于修饰只有一个参数的类构造函数, 它的作用是表明该构造函数是显示的, 而非隐式的, 跟它相对应的另一个关键字是implicit, 意思是隐藏的,类构造函数默认情况下即声明为implicit(隐式). 例如对于一个参数的构造函数来说:
class test { test(int a){} test(double a,int b = 0){} };当我们使用类似test a = 5;这种语句的时候,编译器会默认调用含有一个参数的构造函数给对象a赋值。有时候为了避免这种情况,所以我们可以利用explicit关键字来避免这种隐式的转换。
为了访问其他编译单元(如另一代码文件)中的变量或对象,对普通类型(包括基本数据类、结构和类),可以利用关键字extern,来使用这些变量或对象时;但是对模板类型,则必须在定义这些模板类对象和模板函数时,使用标准C++新增加的关键字export(导出/出口/输出)。例如:
extern int n; extern struct Point p; extern class A a; export template<class T> class stack<int> s; export template<class T> void test (T& t) {...}函数模版的编译模式分两种:完包含编译模式和局部编译模式(需要用export关键字)
举例说明完全包含编译模式: (1)test.h中在声明的后面include “test.cpp”,这样做的目的是把sum的声明和定义放在两个文件中 (2)first.cpp和second.cpp中都用到了sum函数模版,所以都要include “test.h”,所以在两个cpp文件中都有一份sum的定义,所以在实例化以后,存在两个相同的函数定义:int sum(int a,int b){return a+b;} (3)对于这种重复定义的问题,完全包含编译模式下,编译器自己会去除冗余的函数定义,而只保留一个int sum(int a,int b)函数的定义 (4)所以这种编译模式下,编译效率会降低(因为如果100的cpp文件中都调用了sum(2,3),那么会存在100个int sum(int a,int b)函数的定义,去除其余99个冗余定义的操作会占用大量的编译时间)
test.h头文件的内容: template<typename Type> Type sum(Type a,Type b);//函数模版的声明 #include "test.cpp" test.cpp文件的内容: template<typename Type> Type sum(Type a,Type b) { return a+b; }//函数模版的定义 first.cpp文件的内容: #include "test.h" int main() { sum(2,3); return 0; } second.cpp文件的内容: #include "test.h" int second() { sum(2,3); return 0; }举例说明局部编译模式 (1)test.h头文件中不需要再include “test.cpp” (2)需要使用sum的cpp文件中只需要include “test.h”即可 (3)在实例化sum函数时,编译器会自动跟踪到sum函数模版的定义(通过export关键字) (4)这样就提高了编译速度
test.h头文件的内容: template<typename Type> Type sum(Type a,Type b); //函数模版的声明 //#include "test.cpp" //不需要包含sum函数的定义 test.cpp文件的内容: export template<typename Type> //前面加上export关键字 Type sum(Type a,Type b) { return a+b; } //函数模版的定义 first.cpp文件的内容: #include "test.h" int main() { sum(2,3); return 0; } second.cpp文件的内容: #include "test.h" int second() { sum(2,3); return 0; }register修饰符暗示编译程序相应的变量将被频繁地使用,如果可能的话,应将其保存在CPU的寄存器中,以加快其存储速度。例如下面的内存块拷贝代码, 但是使用register修饰符有几点限制 ①register变量必须是能被CPU所接受的类型。 这通常意味着register变量必须是一个单个的值,并且长度应该小于或者等于整型的长度。不过,有些机器的寄存器也能存放浮点数。 ②因为register变量可能不存放在内存中,所以不能用“&”来获取register变量的地址。 ③只有局部自动变量和形式参数可以作为寄存器变量,其它(如全局变量)不行。 在调用一个函数时占用一些寄存器以存放寄存器变量的值,函数调用结束后释放寄存器。此后,在调用另外一个函数时又可以利用这些寄存器来存放该函数的寄存器变量。 ④局部静态变量不能定义为寄存器变量。不能写成:register static int a, b, c; ⑤由于寄存器的数量有限(不同的cpu寄存器数目不一),不能定义任意多个寄存器变量,而且某些寄存器只能接受特定类型的数据(如指针和浮点数),因此真正起作用的register修饰符的数目和类型都依赖于运行程序的机器,而任何多余的register修饰符都将被编译程序所忽略。
①操作符重载 C++可以通过operator实现重载操作符,格式如下:类型T operator 操作符 (),比如重载+,比如下面这个例子 template class A { public: const T operator+(const T& rhs) { return this->m_ + rhs; } private: T m_; };
又比如STL中的函数对象,重载(),比如下面这个例子 template struct A { T operator()(const T& lhs, const T& rhs){ return lhs-rhs; } };
② 操作隐式转换 C++可以通过operator实现重载隐式转换,格式如下: operator T (),其中T是一个类型,比如下面这个例子 class A { public: operator B*() { return this->b_; } operator const B*() { return this->b_; } operator B&() { return *this->b_; } private: B* b_; };
A a; 当if(a),编译时,它转换成if(a.operator B*()),其实也就是相当于 if(a.b_)
volatile的语法与const是一样的,但是volatile的含义是‘在编译器认识的范围外,这个数据可以改变’。所以volatile告诉编译器不要擅自做出有关数据的任何假定,在优化期间这是特别重要的。精确的说编译器在用到这个变量的时候必须每次都小心的重新读取这个变量的值,而不是使用保存在寄存器里的备份。 ①一个参数可以使既是const又是volatile,一个指针也可以是volatile。
①函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍能维持上次的值。 ②在模块内的static全局变量可以被模块内的所有函数访问,但不能被模块外的其它函数访问。 ③在模块内的static函数只可被这一模块内的其它函数访问,这个函数的使用范围被限定在声明它的模块内。 ④在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝。 ⑤在类中的static成员函数属于整个类所拥有,这个函数不接受this指针,因而只能访问类的static成员变量。
C++11 auto auto可以在声明变量的时候根据变量初始值的类型自动为此变量选择匹配的类型,类似的关键字还有decltype。举个例子:
int a = 10; auto au_a = a;//自动类型推断,au_a为int类型 cout << typeid(au_a).name() << endl;typeid运算符可以输出变量的类型。程序的运行结果输出了
int这种用法就类似于C#或Java中的var关键字。auto的自动类型推断发生在编译期,所以使用auto并不会造成程序运行时效率的降低。而是否会造成编译期的时间消耗,我认为是不会的,在未使用auto时,编译器也需要得知右操作数的类型,再与左操作数的类型进行比较,检查是否可以发生相应的转化,是否需要进行隐式类型转换。
auto的用法 上面举的这个例子很简单,在真正编程的时候也不建议这样来使用auto,直接写出变量的类型更加清晰易懂。下面列举auto关键字的正确用法。
用于代替冗长复杂、变量使用范围专一的变量声明。
想象一下在没有auto的时候,我们操作标准库时经常需要这样:
#include<string> #include<vector> int main() { std::vector<std::string> vs; for (std::vector<std::string>::iterator i = vs.begin(); i != vs.end(); i++) { //... } }这样看代码写代码实在烦得很。有人可能会说为何不直接使用using namespace std,这样代码可以短一点。实际上这不是该建议的方法(c++Primer对此有相关叙述)。使用auto能简化代码:
#include<string> #include<vector> int main() { std::vector<std::string> vs; for (auto i = vs.begin(); i != vs.end(); i++) { //.. } }for循环中的i将在编译时自动推导其类型,而不用我们显式去定义那长长的一串。
在定义模板函数时,用于声明依赖模板参数的变量类型。
template <typename _Tx,typename _Ty> void Multiply(_Tx x, _Ty y) { auto v = x*y; std::cout << v; }若不使用auto变量来声明v,那这个函数就难定义啦,不到编译的时候,谁知道x*y的真正类型是什么呢?
模板函数依赖于模板参数的返回值
template <typename _Tx, typename _Ty> auto multiply(_Tx x, _Ty y)->decltype(_Tx*_Ty) { return x*y; }当模板函数的返回值依赖于模板的参数时,我们依旧无法在编译代码前确定模板参数的类型,故也无从知道返回值的类型,这时我们可以使用auto。格式如上所示。 decltype操作符用于查询表达式的数据类型,也是C++11标准引入的新的运算符,其目的也是解决泛型编程中有些类型由模板参数决定,而难以表示它的问题。 auto在这里的作用也称为返回值占位,它只是为函数返回值占了一个位置,真正的返回值是后面的decltype(_Tx*_Ty)。为何要将返回值后置呢?如果没有后置,则函数声明时为:
decltype(_Tx*_Ty)multiply(_Tx x, _Ty y)而此时_Tx,_Ty还没声明呢,编译无法通过。
回到顶部 注意事项 auto 变量必须在定义时初始化,这类似于const关键字。 定义在一个auto序列的变量必须始终推导成同一类型。例如:
auto a4 = 10, a5 = 20, a6 = 30;//正确 auto b4 = 10, b5 = 20.0, b6 = 'a';//错误,没有推导为同一类型使用auto关键字做类型自动推导时,依次施加一下规则: 如果初始化表达式是引用,则去除引用语义。
int a = 10; int &b = a; auto c = b;//c的类型为int而非int&(去除引用) auto &d = b;//此时c的类型才为int& c = 100;//a =10; d = 100;//a =100;如果初始化表达式为const或volatile(或者两者兼有),则除去const/volatile语义。
const int a1 = 10; auto b1= a1; //b1的类型为int而非const int(去除const) const auto c1 = a1;//此时c1的类型为const int b1 = 100;//合法 c1 = 100;//非法如果auto关键字带上&号,则不去除const语意。
const int a2 = 10; auto &b2 = a2;//因为auto带上&,故不去除const,b2类型为const int b2 = 10; //非法这是因为如果去掉了const,则b2为a2的非const引用,通过b2可以改变a2的值,则显然是不合理的。 初始化表达式为数组时,auto关键字推导类型为指针。
int a3[3] = { 1, 2, 3 }; auto b3 = a3; cout << typeid(b3).name() << endl;程序将输出
int *若表达式为数组且auto带上&,则推导类型为数组类型。
int a7[3] = { 1, 2, 3 }; auto & b7 = a7; cout << typeid(b7).name() << endl;程序输出
int [3]函数或者模板参数不能被声明为auto
void func(auto a) //错误 { //... }时刻要注意auto并不是一个真正的类型。 auto仅仅是一个占位符,它并不是一个真正的类型,不能使用一些以类型为操作数的操作符,如sizeof或者typeid。
cout << sizeof(auto) << endl;//错误 cout << typeid(auto).name() << endl;//错误