我本人只有一点C基础,还是上大学的时候习得的。工作不管是开发桌面程序Winform,WPF,还是Web程序,亦或是Office插件,一直使用的C#。在最近的工作中,需要使用C#调用一些C++类库,通常的做法就是把C++类库通过C++ CLI包装一下,然后变成dll直接供C#调用。但是到了.NET Core时代,C++ CLI已经不被支持了,所以唯一的交互方式就是通过P/Invoke,于是看了另外一本书《精通.NET互操作:P/Invoke、C++ Interop和COM.Interop》,发现在.NET与C++进行互操作时有非常多需要注意的地方,一不小心就会有性能问题,且容易出错,当然,这里面涉及到需要掌握一些C++的知识,所以就打算学习一下C++。

    要学习一门新语言,当然要找最经典的书本看,然后跟着练习,于是我找到了《Primer C++ 第五版》这本书,我初次接触到C++里面的一些概念和用法时,感觉到相当的不适,它的概念众多,而且一不留神就容易出错,所以我这里记录一下,权当作为学习笔记。

    C++里面的符合类型是指基于其他类型定义的类型,最基本的是引用和指针。

引用


    引用并非对象,它只是为已经存在的对象起另外一个名字。可以通过&d的形式来定义已用类型,d是变量名,引用必须指向另外一个变量,且必须被初始化,一旦初始化完成,引用就和它的初始值对象一直绑定在一起,无法将引用重新绑定到另外一个对象,所以引用必须初始化

int ival = 1024;
int &refVal = ival;    // refVal指向ival,它是ival的另外一个名字
int &refVal2;          //错误,引用必须被初始化
int &refVal = 3;       //错误,引用必须指向一个对象
refVal = 3;            //把3赋值给refVal指向的对象,就是给ival赋值为3
int i = refVal;        //相当于i=ival;
int &refVal3 = refVal; //将refVal3绑定到refValue绑定的对象上,就是绑定到ival上

    可以在一条语句中定义多个引用,每个标识符都必须以符号&开头:

int i = 1024, i2 = 2048; // i和i2都是int
int &r = i, r2 = i2;     // r是引用,他和i绑定在一起,r2是int类型,初始值为i2
int i3 = 1024, &ri = i3; // i3是int,ri是引用,与i3绑定在一起
int &r3 = i3, &r4 = i2;  // r3和r4都是引用

    除了两种特殊情况,其他所有引用的类型,都要和绑定的对象严格匹配,引用只能绑定对象,不能与字面值或者表达式的结果绑定到一起。

int &refVal4 = 10; //错误,引用类型初始值必须是一个对象
double dval = 3.14;
int &refVal5 = dval; //错误,引用类型的初始值类型必须和引用类型相同,这里是int

     在定义引用类型的时候,必须在类型前面加&,在使用引用类型的时候,就不需要加&了,跟其引用的类型一样使用,只是相当于起了一个别名。赋值的时候,需要加&,取值的时候,不需要加&

指针


    指针是指向另外一种类型的复合类型。它本质上是一个整数,用来存储对象的内存地址。它和引用类型一样也实现了对其它对象的间接访问,但它与引用相比有很多不同,首先,指针本身就是一个对象,允许对指针进行赋值和拷贝,且在生命周期类它可以先后指向几个不同的对象。另外,它不必在定义的时候赋值,在作用域内定义的指针如果没有初始化,就将拥有不确定的值。

    定义指针类型的方式是将声明符写成*d的形式,d是变量名。如果一条语句中定于多个指针变量,则每个变量前面都必须有符号*:

int *ip1, *ip2;  // ip1和iP2都是指向int型对象的指针
double dp, *dp2; // dp是double类型,dp2是指向double类型的指针

    指针存放某个对象的地址,要获取对象的地址,需要使用取地址符&:

int ival = 44;
int *p = &ival; // p存放变量ival的地址,或者说p是指向变量ival的指针

    因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。

    除了两种例外,其他所有指针的类型都要和他所指向的对象严格匹配。

double dval;
double *pd = &dval; //初始值是double类型对象的地址
double *pd2 = pd;   //初始之是指向double对象的指针
int *pi = pd;       //错误:指针pi的类型和pd类型不匹配
pi = &dval;         //错误:试图把double型对象的地址赋值给int型指针

利用指针访问对象


     如果指针指向一个对象,则可以使用解引用符*来访问该对象:

int ival = 44;
int *p = &ival;     // p存放变量ival的地址,或者说p是指向变量ival的指针
cout << *p << endl; //有符号*得到指针p所指的对象,输出44
*p = 0;             //有符号*得到指针所指的对象,即可经由p为变量ival赋值
cout << *p << endl; //输出0

     给解引用的结果赋值,实际上就是给指针所指的对象赋值。

     某些符号具有多重含义,像&和*,技能用作表达式里的运算符,也能作为声明的一部分出现,符号的上下文决定了符号的意义。

int i = 42;
int &r = i;   //&紧随类型名出现,因此是声明的一部分,r是一个引用
int *p;       //*紧随类型名出现,因此是声明的一部分,p是一个指针
p = &i;       //&出现在表达式中,是取地址符
*p = i;       //*出现在表达式中,是解引用符
int &r2 = *p; //&是声明的一部分,*是一个解引用符

    在声明语句中,&和*用于组成复合类型;在表达式中,他们的角色又转变成运算符。在不同场景下出现的虽然是同一个符号,但是含义截然不同,所以可以把它当成不同的符号来看待。

空指针


     不指向任何对象的指针就是空指针,在使用指针之前,需要检查它是否为空,可以使用nullptr来判断,使用nullptr,0或者NULL,也能初始化空指针。在新标准下最好使用nullptr,同时尽量避免使用NULL,可以把0赋值给指针表示不指向任何对象,但把int变量直接赋值给指针是错误的,即使int变量的值恰好等于0

赋值和指针


    指针和引用都能提供对其它对象的间接访问,但在具体实现细节上有很大不同,其中最重要的一点是引用本身并非一个对象,一旦定义了引用,就无法将其再绑定到其他对象,最后每次使用这个引用都是访问它最初绑定的那个对象。

    指针和它存放的地址就没有这种限制,给指针赋值就是让他存放一个新的地址,从而指向了一个新的对象。

int i = 42;
int *pi = 0;   // pi初始化,但没指向任何对象
int *pi2 = &i; // pi2初始化,存的i的地址,指向了i
int *pi3;      //定义了pi3,但他的值是无法确定的
pi3 = pi2;     // pi3和pi2指向同一个对象i
pi2 = 0;       // pi2设置为了空指针,现在pi2不指向任何对象了

     有时候需要搞清楚一条赋值语句到底是改变了指针的值,还是改变了指针所指对象的值不太容易,最好的办法就是记住赋值永远改变的是等号左侧的对象

int ival = 44;
pi = &ival; // pi的值被改变,现在pi存的ival的地址,它指向了ival
*pi = 0;    // ival的值被改变了,指针pi并没有改变,他仍然指向的是ival.

void*指针


     void*指针是一种特殊类型的指针,它可以存放任意对象的地址,不同的是,我们对该地址中到底是个什么类型的对象并不了解。

double obj = 3.14, *pd = &obj;
void *pv = &obj; // void*能存放任意类型对象的地址,obj可以是任意类型的对象,这里是double类型。
pv = pd;         // pv可以存放任意类型的指针

指针和引用的简单区别


    引用只是对指针进行了简单的封装,它的底层依然是通过指针实现的,本质是一个指针常量。

int a = 10;
int &b = a;
//上面的引用相当于如下定义
int *const b = &a;

     我们知道指针常量是不能再指向其他变量的,也就是它的地址不能变了,所以b只能作为a的别名,而不能再作为其他变量的别名,即引用一旦初始化后,就不可以发生改变。同时,指针常量虽然不能再指向其他变量,但是它指向变量的值可以改变,即

*b=20;

    是正确的,也就是此时a为20。所以对于引用b来说,

b=20;

    也是正确的,可以通过别名改变这个变量的值,相当于C++内部自动转换为*b=20;

    引用占用的内存和指针占用的内存长度一样,在 32 位环境下是 4 个字节,在 64 位环境下是 8 个字节,之所以不能获取引用的地址,是因为编译器进行了内部转换。引用虽然是基于指针实现的,但它比指针更加易用,从上面的两个例子也可以看出来,通过指针获取数据时需要加*,书写麻烦,而引用不需要,它和普通变量的使用方式一样。C++ 的发明人 Bjarne Stroustrup 也说过,他在 C++ 中引入引用的直接目的是为了让代码的书写更加漂亮,尤其是在运算符重载中,不借助引用有时候会使得运算符的使用很麻烦。通过下面的两个方法可以更加明显的看到这一点。

    假设我们有一个函数,可以对int类型的对象进行自增,默认函数调用时按值传递的,所以要改变传入对象的值,必须传地址,传地址可以有两种方式,指针和引用:

#include <iostream>
using namespace std;

void increase(int a)
{
    a++;
}

void increase_by_pointer(int *a)
{
    (*a)++;
}

void increase_by_ref(int &a)
{
    a++;
}

int main()
{
    int x = 5;
    cout << "original:" << x << endl;
    increase(x);
    cout << "after call increase:" << x << endl;
    x = 5;
    increase_by_pointer(&x);
    cout << "after call increase_by_pointer:" << x << endl;
    x = 5;
    increase_by_ref(x);
    cout << "after call increase_by_ref:" << x << endl;
    return 0;
}

    可以看到采用传引用的方式的方法 increase_by_ref 比传指针 increase_by_pointer 的方法更加简洁和易懂。

复合类型的声明


    变量的定义包括一个基本的数据类型和一组声明符。在同一条定于语句中,虽然基本数据类型只有一个,但是声明符形式却可以不同,也就是说一条定义语句可以定义出不同类型的变量。

// i是int型的数,p是一个int型指针,r是一个int型引用
int i = 1024, *p = &i, &r = i;

    设计指针或声明有两种写法,一种是把修饰符和变量标识符写在一起,比如:

int *p1, *p2; // p1和p2都是指向int的指针

    第二种写法是把修饰符和类型名写在一起,并且每条语句只定义一个变量:

int* p1;//p1是指向int的指针
int* p2;//p2是指向int的指针

      后面这种把修饰符和类型写在一起的,是合法但容易产生误导,比如:

int *p1, p2; // p1是指向int的指针,p2是int类型

    所以一般推荐第一种方法,或者使用第二种方法的时候,一条语句只定义一个变量。

指向指针的指针


    声明符中的修饰符的个数并没有限制,当有多个修饰符连写在一起时,按照其逻辑关系解释就可以。比如指针是内存中的对象,像其它对象一样,它也有自己的地址,因此允许把指针的地址再存放到另外一个地址中:

int ival = 1024;
int *pival = &ival;
cout << " the ival address is " << &ival << endl;//输出 ival的地址,输出结果为0x61fe0c
cout << " pival point address is " << pival << endl;//输出pival指针所指的地址,结果为0x61fe0c 
cout << " the address of pival is " << &pival << endl;//输出pival指针对象本身自己的地址,结果为0x61fe00

    通过*的个数可以区分指针的级别,也就是说**表示指向指针的指针,***表示指向指针的指针的指针,以此类推:

int ival = 1024;
int *pival = &ival;//pival指向一个int型
int **ppival = &pival;//ppival指向一个int型的指针
cout << " the value of ival" << endl;
cout << " direct value " << ival << endl;
cout << " indirect value " << *pival << "\n";
cout << " double indirect value " << **ppival << endl;

    上面的程序使用三种不同的方式输出了ival的值,第一种是直接输出,第二种是通过int型指针pival输出,第三种通过两次解引用ppival,得到ival的值。

指向指针的引用


    引用本身不是一个对象,所以不能定义指向引用的指针。但是指针是对象,所以存在对指针的引用,相当于给指针起一个别名。

int i = 42;
int *p;      // p是一个int型指针
int *&r = p; // r是一个引用,他是一个对指针p的引用,也就是指针p的别名
r = &i;      // r是对指针p的引用,因此给r赋值&i,就是相当于给p赋值i的地址,效果等同于 p=&i;
*r = 0;      // r解引用,得到i对象,也就是p指向的对象,将i的值改为0,相当于*p=0;

     上面第三句,要理解r到底是什么,最简单的办法是从右向左阅读r的定义。离变量名最近的符号(&r种的&符号)对变量有最直接的影响,所以r是一个引用。声明符的其余部分用以确定r引用的类型是什么,这里的符号*说明r引用的是一个指针。最后合起来,就是声明的基本数据类型部分指出,r引用的是一个指向int的指针。

const限定符


     如果希望定义一种变量,它的值不能被改变,那么就可以使用关键字const。因为const对象一旦创建了它的值就不能再改变,所以const对象必须初始化,

const int bufferSize = 512;
bufferSize = 512;         //错误:试图想const对象写值
const int i = get_size(); //正确:运行时初始化
const int j = 42;         //正确:编译时初始化
const int k;              //错误:k是一个常量,但未初始化

const的引用


    可以把引用绑定到const对象上,就像绑定到其他对象上一样,这就是对常量的引用。与普通引用不同的是,对常量的引用不能被用作修改他所绑定的对象:

const int ci = 1024;
const int &r1 = ci; // r1是对int型常量的引用
r1 = 42;            //错误:r1是对常量的引用,常量不能被修改
int &r2 = ci;       //错误:试图让一个非常量引用,指向一个常量对象。

    因为不允许直接对ci赋值,所以当然也不能通过引用r1去改变ci。r2定义的是一个指向int型的引用,他能够用来修改所指向的int对象,因为ci是常量类型,不能被修改,所以对r2的初始化是错误的。

初始化和对const的引用


     在前面提到引用类型时说到,引用的类型必须与其所引用对象的类型一致,但有两个例外。一个例外是,初始化常量引用时,允许用任意的表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。另外,允许为一个常量引用绑定到非常量的对象、字面值,甚至是一般的表达式。

int i = 42;
const int &r1 = i;      //允许将const int &绑定到一个普通的int对象上
const int &r2 = 42;     // r2是一个常量引用,这里可以直接赋值一个常量
const int &r3 = r1 * 2; // r3是一个常量引用
int &r4 = r1 * 2;       // r4是一个普通的非常量引用

     注意,可以直接将常量值赋值给一个常量引用,而之前在讲引用的时候,是无法直接讲一个常量赋值给引用的。当将一个普通int类型,绑定要另外一种类型时。可以看到到底发生了什么?

double dval = 3.14;
const int &ri = dval;

    为了确保ri指向一个常量证书,编译器把上面的代码变成了如下:

const int temp = dval; //由双精度浮点数生成一个临时的整型常量
const int &ri = temp;  //让ri绑定这个临时量

      还有一些需要注意的细节,如下:

double dval = 3.14;
const int &ri = dval; //合法,编译器会生成一个临时整型常量,用dval初始化,让ri绑定这个临时量
int &r2 = dval;       //非法,不能将一个int引用绑定到double类型上
const int &r3 = 4;    //合法,能够直接使用常量值初始化一个常量引用
int &r4 = 5;          //非法,引用不是对象,他必须引用其他对象

       需要注意到,const引用可能引用的并非是一个const的对象,const引用只是对const本身的限制。

int i = 42;
int &r1 = i;                  //引用绑定对象i
const int &r2 = i;            // r2也绑定到对象i,但是因为是常量引用,所以不允许通过r2修改i的值,但是i的值可以通过其他非常量引用比如r1来修改
std::cout << r2 << std::endl; //输出 42
r1 = 0;
r2 = 0;                       //非法:r2是一个常量引用
std::cout << r2 << std::endl; //输出 0

指针和const


    和引用一样,也可以让指针指向常量或非常量。类似于常量引用,指向常量的指针,不能用于改变所指对象的值。要想存放常量对象的地址,只能使用指向常量的指针。

const double pi = 3.14;   // pi是常量,它的值不能改变
double *ptr = &pi;        //错误,ptr是一个普通指针,不能指向常量
const double *cptr = &pi; //正确,cptr可以指向一个双精度常量
*cptr = 42;               //错误,不能给*cptr赋值

     另外,允许一个指向常量的指针指向一个非常量对象。

double dval = 3.47; // dval是一个双精度浮点数,它的值可以改变
cptr = &dval;       //正确,但不同通过cptr改变dval的值

    与常量指针一样,指向常量的指针没有对顶所指的对象必须是一个常量。指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变。另外,可以看到指向常量的指针指的是指针所指的对象不能通过指针来改变,指针本身可以改变,重新指向另外一个对象,比如上面的cptr一开始指向的是pi,后面让他重新指向了dval,但是我们不能通过该指针修改所指对象的值。

const指针


    指针是对象而引用不是,所以可以像其它对象类型一样把指针本身定义为常量。常量指针必须初始化,一旦初始化完成之后,它的值(也就是存放指针中的那个地址)就不能再改变了。把*放在const关键字之前说明该指针是一个常量。它意味着不能改变指针本身的值,但是通过指针可以改变其所指对象的值。

int errNum = 0;
int *const currErr = &errNum; // currErr将一直指向errNum
const double pi2 = 3.14159;
const double *const pip = &pi2; // pip是一个指向常量对象的常量指针

    要弄清具体类型,最行之有效的办法是从右向左读,比如离currErr最近的符号是const,表示currErr本身是一个常量对象,对象类型由声明符的其余部分确定。声明符的下一个符号是*,意思是curErr是一个常量指针。最后基本数据类型部分确定了常量指针指向的是一个int对象。依次类推,可以推断出pip是一个常量指针,它指向的对象是一个双精度浮点型的常量。

    指针本身是一个常量并不意味着不能通过指针修改其所指向对象的值,能否这样做完全取决于所指对象的类型。比如pip是一个指向常量的常量指针,则不论是pip所指的对象,还是pip自己存储的那个地址(pip本身)都不能改变。相反,curErr指向的是一个一般的非常量对象,那么完全可以用currErr来修改errNum的值。

*pip = 3.75;     //错误:pip是一个指向常量的指针,不能通过指针来修改所指对象pi2的值
pip = &dval;     //错误:pip是一个常量指针,一单初始化后,不能修改其所只想的对象
*currErr = 32;   //正确:curErr所指的是一个非常量,所以可以通过指针修改所指对象的值
currErr = &dval; //错误:currErr是常量指针

顶层const


    指针本身是一个对象,它又可以指向另外一个对象。因此,指针本身是不是常量以及指针所指的对象是不是一个常量就是两个相互独立的问题。用名词顶层 const (top level const)表示指针本身是一个常量,而用名词底层const(low level const)表示指针所指的对象是一个常量

    更一般的,顶层const可以表示任意的对象是常量,这一点对任何数据类型都适用,如算术类型、类、指针等。底层const则与指针和引用等符合类型的基本类型部分有关。比较特殊的是,指针类型既可以是顶层const也可以是底层const,这一点和其它类型区别明显。

int i = 0;
int *const p1 = &i;       //顶层const,不能改变pi的值,常量指针,指向int
const int ci = 42;        //顶层const,不能改变ci的值
const int *p2 = &ci;      //底层const,p2是一个指向常量的指针,可以改变p2让他指向其他的值,但是不能通过p2来改变所指对象的值。
const int *const p3 = p2; //右边是顶层const,左边是底层const。p3,是一个常量指针,他指向一个int型常量。
const int &r = ci;        //用于声明引用的const都是底层的const,ri是一个引用他引用的类型是int型常量

     在执行对象拷贝时,常量是顶层const还是底层const区别明显,其中顶层const不受影响:

i = ci;  //正确:拷贝ci的值,ci是一个顶层的const
p2 = p3; //正确,p2和p3指向的底层对象类型相同,p3顶层const的部分不影响

     执行拷贝操作并不会改变被拷贝对象的值,因此,拷入和拷出的对象是否是常量没有影响。另一方面,底层const的限制却不能忽视,当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换为常量,反之则不行。

int *p = p3;       //错误:p3是一个常量指针,他指向一个int型常量,p3包含底层const的定义,而p没有
p2 = p3;           //正确:p2和p3都是底层const
p2 = &i;           //正确:int*能够转换为const int*
int &r = ci;       //错误:普通的int&不能绑定到int常量上
const int &r2 = i; //正确:const int &可以绑定到普通的int上

    p3既是顶层const也是底层const,拷贝p3时可以不在乎他是一个顶层const,但是必须清楚它指向的对象是一个常量。因此,不能用p3去初始化p,因为p指向的是一个普通整数;另外,p3的值可以赋给p2,是因为这两个指针都是底层const,尽管p3同时也是一个常量指针(顶层const),但仅就这次赋值而言不会有什么影响。