• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

【C++】从设计原理来看string类

原作者: [db:作者] 来自: [db:来源] 收藏 邀请

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, 
                      

鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
上一篇:
C++学习记录1发布时间:2022-07-18
下一篇:
聊聊C++中的几种智能指针(上)发布时间:2022-07-18
热门推荐
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap