1、一些C++基础知识
模板类string的设计属于底层,其中运用到了很多C++的编程技巧,比如模板、迭代器、友元、函数和运算符重载、内联等等,为了便于后续理解string类,这里先对涉及到的概念做个简单的介绍。C++基础比较扎实的童鞋可以直接跳到第三节。
1.1 typedef
1.1.1 四种常见用法
- 定义一种类型的别名,不只是简单的宏替换。可用作同时声明指针型的多个对象
typedef char* PCHAR; PCHAR pa, pb; // 同时声明两个char类型的指针pa和pb char* pa, pb; // 声明一个指针(pa)和一个char变量(pb) // 下边的声明也是创建两个char类型的指针。但相对没有typedef的形式直观,尤其在需要大量指针的地方 char *pa, *pb;
顺便说下,*运算符两边的空格是可选的,在哪里添加空格,对于编译器来说没有任何区别。
char *pa; // 强调*pa是一个char类型的值,C中多用这种格式。 char* pa; // 强调char*是一种类型——指向char的指针。C++中多用此种格式。另外在C++中char*是一种复合类型。
- 定义struct结构体别名
在旧的C代码中,声明struct新对象时,必须要带上struct,形式为:struct 结构名 对象名。
1 // 定义 2 struct StudentStruct 3 { 4 int ID; 5 string name; 6 }; 7 // C声明StudentStruct类型对象 8 struct StudentStruct s1;
使用typedef来定义结构体StrudentStruct的别名为Student,声明的时候就可以少写一个struct,尤其在声明多个struct对象时更加简洁直观。如下:
1 // 定义 2 typedef struct StudentStruct 3 { 4 int ID; 5 string name; 6 }Student; 7 // C声明StudentStruct类型对象s1和s2 8 Student s1, s2;
而在C++中,声明struct对象时本来就不需要写struct,其形式为:结构名 对象名。
// C++声明StudentStruct类型对象s1和s2 StudentStruct s1,s2;
所以,在C++中,typedef的作用并不大。了解他便于我们阅读旧代码。
- 定义与平台无关的类型
比如定义一个REAL的浮点类型,在目标平台一上,让它表示最高精度的类型为:
typedef long double REAL;
在不支持long double的平台二上,改为:
typedef double REAL;
在连double都不支持的平台三上,改为:
typedef float REAL;
也就是说,在跨平台时,只要改下typedef本身就行,不要对其他源码做任何修改。
标准库中广泛使用了这个技巧,比如size_t、intptr_t等
1 // Definitions of common types 2 #ifdef _WIN64 3 typedef unsigned __int64 size_t; 4 typedef __int64 ptrdiff_t; 5 typedef __int64 intptr_t; 6 #else 7 typedef unsigned int size_t; 8 typedef int ptrdiff_t; 9 typedef int intptr_t; 10 #endif
-
为复杂的声明定义一个新的简单的别名
在阅读代码的过程中,我们经常会遇到一些复杂的声明和定义,例如:
1 // 理解下边这种复杂声明可用“右左法则”: 2 // 从变量名看起,先往右,再往左,碰到一个圆括号就调转阅读的方向;括号内分析完就跳出括号,还是按先右后左的顺序,如此循环,直到整个声明分析完。 3 4 // 例1 5 void* (*(*a)(int))[10]; 6 // 1、找到变量名a,往右看是圆括号,调转方向往左看到*号,说明a是一个指针; 7 // 2、跳出内层圆括号,往右看是参数列表,说明a是一个函数指针,接着往左看是*号,说明指向的函数返回值是指针; 8 // 3、再跳出外层圆括号,往右看是[]运算符,说明函数返回的是一个数组指针,往左看是void*,说明数组包含的类型是void*。 9 // 简言之,a是一个指向函数的指针,该函数接受一个整型参数并返回一个指向含有10个void指针数组的指针。 10 11 // 例2 12 float(*(*b)(int, int, float))(int);// 1、找到变量名b,往右看是圆括号,调转方向往左看到*号,说明b是一个指针; 13 // 2、跳出内层圆括号,往右看是参数列表,说明b是一个函数指针,接着往左看是*号,说明指向的函数返回值是指针; 14 // 3、再跳出外层圆括号,往右看还是参数列表,说明返回的指针是一个函数指针,该函数有一个int类型的参数,返回值类型是float。 15 // 简言之,b是一个指向函数的指针,该函数接受三个参数(int, int和float),且返回一个指向函数的指针,该函数接受一个整型参数并返回一个float。 16 17 // 例3 18 double(*(*(*c)())[10])(); 19 // 1、先找到变量名c(这里c其实是新类型名),往右看是圆括号,调转方向往左是*,说明c是一个指针; 20 // 2、跳出圆括号,往右看是空参数列表,说明c是一个函数指针,接着往左是*号,说明该函数的返回值是一个指针; 21 // 3、跳出第二层圆括号,往右是[]运算符,说明函数的返回值是一个数组指针,接着往左是*号,说明数组中包含的是指针; 22 // 4、跳出第三层圆括号,往右是参数列表,说明数组中包含的是函数指针,这些函数没有参数,返回值类型是double。 23 // 简言之,c是一个指向函数的指针,该函数无参数,且返回一个含有10个指向函数指针的数组的指针,这些函数不接受参数且返回double值。 24 25 // 例4 26 int(*(*d())[10])(); // 这是一个函数声明,不是变量定义 27 // 1、找到变量名d,往右是一个无参参数列表,说明d是一个函数,接着往左是*号,说明函数返回值是一个指针; 28 // 2、跳出里层圆括号,往右是[]运算符,说明d的函数返回值是一个指向数组的指针,往左是*号,说明数组中包含的元素是指针; 29 // 3、跳出外层圆括号,往右是一个无参参数列表,说明数组中包含的元素是函数指针,这些函数没有参数,返回值的类型是int。 30 // 简言之,d是一个返回指针的函数,该指针指向含有10个函数指针的数组,这些函数不接受参数且返回整型值。
如果想要定义和a同类型的变量a2,那么得重复书写:
void* (*(*a)(int))[10]; void* (*(*a2)(int))[10];
那怎么避免这种没有价值的重复呢?答案就是用typedef来简化复杂的声明和定义。
// 在之前的定义前边加typedef,然后将变量名a替换为类型名A typedef void* (*(*A)(int))[10]; // 定义相同类型的变量a和a2 A a, a2;
typedef在这里的用法,总结一下就是:任何声明变量的语句前面加上typedef之后,原来是变量的都变成一种类型。不管这个声明中的标识符号出现在中间还是最后。
1.1.2 使用typedef容易碰到的陷进
- 陷进一
typedef定义了一种类型的新别名,不同于宏,它不是简单的字符串替换。比如:
typedef char* PSTR; int mustrcmp(const PSTR, const PSTR);
上边的const PSTR并不是const char*,而是相当于.char* const。原因在于const给予了整个指针本身以常量性,也就是形成常量指针char* const。
- 陷进二
typedef在语法上是一个存储类的关键字(和auto、extern、mutable、static、register等一样),虽然它并不真正影响对象的存储特性。如:
typedef static int INT2;
编译会报错:“error C2159:指定了一个以上的存储类”。
1.2 #define
#define是宏定义指令,宏定义就是将一个标识符定义为一个字符串,在预编译阶段执行,将源程序中的标志符全部替换为指定的字符串。#define有以下几种常见用法:
- 无参宏定义
格式:#define 标识符 字符串
其中的“#”表示这是一条预处理命令。凡是以“#”开头的均为预处理命令。“define”为宏定义命令。“标识符”为所定义的宏名。“字符串”可以是常数、表达式、格式串等。
- 有参宏定义
格式:#define 宏名(形参表) 字符串
1 #define add(x, y) (x + y) //此处要打括号,不然执行2*add(x,y)会变成 2*x + y 2 int main() 3 { 4 std::cout << add(9, 12) << std::endl; // 输出21 5 return 0; 6 }
- 宏定义中的条件编译
在大规模开发过程中,头文件很容易发生嵌套包含,而#ifdef配合#define,#endif可以避免这个问题。作用类似于#pragma once。
#ifndef DATATYPE_H #define DATATYPE_H ... #endif
- 跨平台
在跨平台开发中,也常用到#define,可以在编译的时候通过#define来设置编译环境。
1 #ifdef WINDOWS 2 ... 3 (#else) 4 ... 5 #endif 6 #ifdef LINUX 7 ... 8 (#else) 9 ... 10 #endif
- 宏定义中的特殊操作符
#:对应变量字符串化
##:把宏定义名与宏定义代码序列中的标识符连接在一起,形成一个新的标识符
1 #include <stdio.h> 2 #define trace(x, format) printf(#x " = %" #format "\n", x) 3 #define trace2(i) trace(x##i, d) 4 5 int main(int argc, _TCHAR* argv[]) 6 { 7 int i = 1; 8 char *s = "three"; 9 float x = 2.0; 10 11 trace(i, d); // 相当于 printf("x = %d\n", x) 12 trace(x, f); // 相当于 printf("x = %f\n", x) 13 trace(s, s); // 相当于 printf("x = %s\n", x) 14 15 int x1 = 1, x2 = 2, x3 = 3; 16 trace2(1); // 相当于 trace(x1, d) 17 trace2(2); // 相当于 trace(x2, d) 18 trace2(3); // 相当于 trace(x3, d) 19 20 return 0; 21 } 22 23 // 输出: 24 // i = 1 25 // x = 2.000000 26 // s = three 27 // x1 = 1 28 // x2 = 2 29 // x3 =3
__VA_ARGS__:是一个可变参数的宏,这个可变参数的宏是新的C99规范中新增的,目前似乎只有gcc支持。实现思想就是宏定义中参数列表的最后一个参数为省略号(也就是三个点)。这样预定义宏__VA_ARGS__就可以被用在替换部分中,替换省略号所代表的字符串,如:
1 #define PR(...) printf(__VA_ARGS__) 2 int main() 3 { 4 int wt=1,sp=2; 5 PR("hello\n"); // 输出:hello 6 PR("weight = %d, shipping = %d",wt,sp); // 输出:weight = 1, shipping = 2 7 return 0; 8 }
附:C++中其他常用预处理指令:
#include // 包含一个源代码文件 #define // 定义宏 #undef // 取消已定义的宏 #if // 如果给定条件为真,则编译下面代码 #ifdef // 如果宏已定义,则编译下面代码 #ifndef // 如果宏没有定义,则编译下面代码 #elif // 如果前面#if给定条件不为真,当前条件为真,则编译下面代码 #endif // 结束一个#if...#else条件编译块 #error // 停止编译并显示错误信息 __FILE__ // 在预编译时会替换成当前的源文件cpp名 __LINE__ // 在预编译时会替换成当前的行号 __FUNCTION__ // 在预编译时会替换成当前的函数名称 __DATE__ // 进行预处理的日期(“Mmm dd yyyy”形式的字符串文字) __TIME__ // 源文件编译时间,格式为“hh:mm:ss”
1.3 typedef VS #define
C++为类型建立别名的方式有两种,一种是使用预处理器#define,一种是使用关键字typedef,格式如下:
#define BYTE char // 将Byte作为char的别名 typedef char byte;
但是在声明一系列变量是,请使用typedef而不是#define。比如要让byte_pointer作为char指针的别名,可将byte_pointer声明为char指针,然后再前面加上typedef:
typedef float* float_pointer;
也可以使用#define,但是在声明多个变量时,预处理器会将下边声明“FLOAT_POINTER pa, pb;”置换为:“float * pa, pb;”,这显然不是我们想要的结果。但是用typedef就不会有这样的问题。
#define FLOAT_POINTER float* FLOAT_POINTER pa, pb;
1.4 using
using关键字常见用法有三:
- 引入命名空间
using namespace std; // 也可在代码中直接使用std::
- 在子类中使用using引入基类成员名称
子类继承父类之后,在public、protected、private下使用“using 可访问的父类成员”,相当于子类在该修饰符下声明了该成员。
- 类型别名(C++11引入)
一般情况下,using与typedef作用等同:
// 使用using(C++11) using counter = long; // 使用typedef(C++03) // typedef long counter;
别名也适用于函数指针,但比等效的typedef更具可读性:
1 // 使用using(C++11) 2 using func = void(*)(int); 3 4 // 使用typedef(C++03) 5 // typedef void (*func)(int); 6 7 // func can be assigned to a function pointer value 8 void actual_function(int arg) { /* ... */ } 9 func fptr = &actual_function;
typedef的局限是它不适用于模板,但是using支持创建类型别名,例如:
template<typename T> using ptr = T*; // the name 'ptr<T>' is now an alias for pointer to T ptr<int> ptr_int;
1.5 typename
- 在模板参数列表中,用于指定类型参数。(作用同class)
template <class T1, class T2>... template <typename T1, typename T2>...
- 用在模板定义中,用于标识“嵌套依赖类型名(nested dependent type name)”,即告诉编译器未知标识符是一种类型。
这之前先解释几个概念:
> 依赖名称(dependent name):模板中依赖于模板参数的名称。
> 嵌套依赖名称(nested dependent name):从属名称嵌套在一个类里边。嵌套从属名称是需要用typename声明的。
template<class T> class X { typename T::Y m_y; // m_y依赖于模板参数T,所以m_y是依赖名称;m_y同时又嵌套在X类中,所以m_y又是嵌套依赖名称 };
上例中,m_y是嵌套依赖名称,需要typename来告诉编译器Y是一个类型名,而非变量或其他。否则在T成为已知之前,是没有办法知道T::Y到底是不是一个类型。
- typename可在模板声明或定义中的任何位置使用任何类型。不允许在基类列表中使用该关键字,除非将它用作模板基类的模板自变量。
1 template <class T> 2 class C1 : typename T::InnerType // Error - typename not allowed. 3 {}; 4 template <class T> 5 class C2 : A<typename T::InnerType> // typename OK. 6 {};
1.6 template
C++提供了模板(template)编程的概念。所谓模板,实际上是建立一个通用函数或类,其类内部的类型和函数的形参类型不具体指定,用一个虚拟的类型来代表。这种通用的方式称为模板。模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码。
1.6.1 函数模板
函数模板是通用的函数描述,也就是说,它们使用泛型来定义函数,其中的泛型可用具体的类型(如int或double)替换。通过将类型作为参数传递给模板,可使编译器生成该类型的函数。由于模板允许以泛型方式编程,因此又被称为通用编程。由于类型用参数表示,因此模板特性也被称为参数化类型(parameterized types)。
请注意,模板并不创建任何函数,而只是告诉编译器如何定义函数。一般如果需要多个将同一种算法用于不同类型的函数,可使用模板。
(1)模板定义
template <typename T> // or template <class T> void f(T a, T b) {...}
在C++98添加关键字typename之前,用class来创建模板,二者在此作用相同。注意这里class只是表明T是一个通用的类型说明符,在使用模板时,将使用实际的类型替换它。
(2)显式具体化(explicit specialization)
- 对于给定的函数名,可以有非模板函数、模板函数和显示具体化模板函数以及他们的重载版本。
- 他们的优先级为:非模板 > 具体化 > 常规模板。
- 显示具体化的原型与定义应以template<>打头,并通过名称来指出类型。
举例如下:
1 #include <iostream> 2 3 // 常规模板 4 template <typename T> 5 void Swap(T &a, T &b); 6 7 struct job 8 { 9 char name[40]; 10 double salary; 11 int floor; 12 }; 13 14 // 显示具体化 15 template <> void Swap<job>(job &j1, job &j2); 16 17 int main() 18 { 19 using namespace std; 20 cout.precision(2); // 保留两位小数精度 21 cout.setf(ios::fixed, ios::floatfield); // fixed设置cout为定点输出格式;floatfield设置输出时按浮点格式,小数点后有6为数字 22 23 int i = 10, j = 20; 24 Swap(i, j); // 生成Swap的一个实例:void Swap(int &, int&) 25 cout << "i, j = " << i << ", " << j << ".\n"; 26 27 job sxx = { "sxx", 200, 4 }; 28 job xt = { "xt", 100, 3 }; 29 Swap(sxx, xt); // void Swap(job &, job &) 30 cout << sxx.name << ": " << sxx.salary << " on floor " << sxx.floor << endl; 31 cout << xt.name << ": " << xt.salary << " on floor " << xt.floor << endl; 32 33 return 0; 34 } 35 36 // 通用版本,交换两个类型的内容,该类型可以是结构体 37 template <typename T> 38 void Swap(T &a, T &b) 39 { 40 T temp; 41 temp = a; 42 a = b; 43 b = temp; 44 } 45 46 // 显示具体化,仅仅交换job结构的salary和floor成员,而不交换name成员 47 template <> void Swap<job>(job &j1, job &j2) 48 { 49 double t1; 50 int t2; 51 t1 = j1.salary; 52 j1.salary = j2.salary; 53 j2.salary = t1; 54 t2 = j1.floor; 55 j1.floor = j2.floor; 56 j2.floor = t2; 57 }
(3)实例化和具体化
- 隐式实例化(implicit instantiation):编译器使用模板为特定类型生成函数定义时,得到的是模板实例。例如,上边例子第23行,函数调用Swap(i, j)导致编译器生成Swap()的一个实例,该实例使用int类型。模板并给函数定义,但使用int的模板实例就是函数定义。这种该实例化fangshi被称为隐式实例化。
- 显示实例化(explicit instantiation):可以直接命令编译器创建特定的实例。语法规则是,声明所需的种类(用<>符号指示类型),并在声明前加上关键字template:
template void Swap<int>(int, int); // 该声明的意思是“使用Swap()模板生成int类型的函数定义”
- 显示具体化(explicit specialization):前边以介绍,显示具体化使用下面两个等价的声明之一:
// 该声明意思是:“不要使用Swap()模板来生成函数定义,而应使用专门为int类型显示定义的函数定义” template <> void Swap<int>(int &, int &); template <> void Swap(int &, int &);
注意:显示具体化的原型必须有自己的函数定义。
以上三种统称为具体化(specialization)。下边的代码总结了上边这些概念:
1 template <class T> 2 void Swap(T &, T &); // 模板原型 3 4 template <> void Swap<job>(job &, job &); // 显示具体化 5 template void Swap<char>(char &, char &); // 显式实例化 6 7 int main(void) 8 { 9 short a, b; 10 Swap(a, b); // 隐式实例化 11 12 job n, m; 13 Swap(n, m); // 使用显示具体化 14 15 char g, h; 16 Swap(g, h); // 使用显式模板实例化 17 }
编译器会根据Swap()调用中实际使用的E参数,生成相应的版本。
当编译器看到函数调用Swap(a, b)后,将生成Swap()的short版本,因为两个参数都是short。当编译器看到Swap(n, m)后,将使用为job类型提供的独立定义(显示具体化)。当编译器看到Swap(g, h)后,将使用处理显式实例化时生成的模板具体化。
(4)关键字decltype(C++11)
- 在编写模板函数时,并非总能知道应在声明中使用哪种类型,这种情况下可以使用decltype关键字:
template <class T1, Class T2> void ft(T1 x, T2 y) { decltype(x + y) xpy = x + y; // decltype使得xpy和x+y具有相同的类型 }
- 有的时候我们也不知道模板函数的返回类型,这种情况下显然是不能使用decltype(x+y)来获取返回类型,因为此时参数x和y还未声明。为此,C++新增了一种声明和定义函数的语法:
// 原型 double h(int x, float y); // 新增的语法 auto h(int x, float y) -> double;
该语法将返回参数移到了参数声明的后面。->double被称为后置返回类型(trailing return type)。其中auto是一个占位符,表示后置返回类型提供的类型。
所以在不知道模板函数的返回类型时,可使用这种语法:
template <class T1, Class T2> auto ft(T1 x, T2 y) -> decltype(x + y) { return x + y; }
1.6.2 类模板
(1)类模板定义和使用
1 // 类模板定义 2 template <typename T> // or template <class T> 3 class A 4 {...} 5 6 // 实例化 7 A<t> st; // 用具体类型t替换泛型标识符(或者称为类型参数)T
程序中仅包含模板并不能生成模板类,必须要请求实例化。为此,需要声明一个类型为模板类对象,方法是使用所需的具体类型替换泛型名。比如用来处理string对象的栈类,就是basic_string类模板的具体实现。
应注意:类模板必须显示地提供所需的类型;而常规函数模板则不需要,因为编译器可以根据函数的参数类型来确定要生成哪种函数。
(2)模板的具体化
类模板与函数模板很相似,也有隐式实例化、显示实例化和显示具体化,统称为具体化(specialization)。模板以泛型的方式描述类,而具体化是使用具体的类型生成类声明。
- 隐式实例化:声明一个或多个对象,指出所需的类型,而编译器使用通用模板提供的处方生成具体的类定义。需要注意的是,编译器在需要对象之前,不会生成类的隐式实例化。
- 显示实例化:使用关键字template并指出所需类型来声明类时,编译器将生成类声明的显示实例化。
1 // 类模板定义 2 template <class T, int n> 3 class ArrayTP 4 {...}; 5 6 // 隐式实例化(生成具体的类定义) 7 ArrayTP<int, 100> stuff 8 9 // 显示实例化(将ArrayTP<string, 100>声明为一个类) 10 template class ArrayTP<string, 100>;
- 显示具体化:是特定类型(用于替换模板中的泛型)的定义。
- 部分具体化(partial specializaiton):即部分限制模板的通用性。
第一:部分具体化可以给类型参数之一指定具体的类型:
1 // 通用模板 2 template <typename T1, typename T2> class Pair {...}; 3 // 部分具体化模板(T1不变,T2具体化为int) 4 template <typename T1> class Pair<T1, int> {...}; 5 // 显示具体化(T1和T2都具体化为int) 6 template <> calss Pair<int, int> {...};
如果有多种模板可供选择,编译器会使用具体化程度最高的模板(显示 > 部分 > 通用),比如对上边三种模板进行实例化:
Pair<double, double> p1; // 使用通用模板进行实例化 Pair<double,
请发表评论