C++指针与引用

对于类型T来说,T是表示“指向T的指针”的;类型。换句话说,T类型的变量能存放T类型对象的地址:

1
2
char c = 'a';
char* p = &c; //p存放c的地址

对指针的一个基本操作是解引用,即引用指针所指的对象。这个操作也称为间接取值。解引用运算符是个前置一元运算符,对应的符号是*:

1
2
3
char c = 'a';
char* p = &c; //p存放c的地址
char c2 = *p; // c2=='a'

当指针指向数组中的元素时,C++允许对这类指针执行某些算数运算。
符号*在用作类型名的的后缀时表示“指向”的含义。如果想表示指向数组的指针或指向函数的指针,需要使用稍微复杂一点的形式:

1
2
3
4
5
int* pi; //指向int的指针
char** ppc; //指向字符指针的指针
int* ap[15]; //ap是一个指针,包含15个指向int的指针
int(*fp)(char*); //指向函数的指针,该函数接受一个char*实参,返回一个int
int* f(char*); //该函数接受一个char*实参,返回一个指向int的指针

void*的含义是“指向未知类型对象的指针”。
除了函数指针和指向类成员的指针,指向其他任意类型对象的指针都能被赋给一个void*类型的变量。要想使用void*,必须将其显式的转换成某一特定类型的指针。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void f(int* pi)
{
    void* pv = pi; //OK,int*到void*的隐式转换
    *pv; //错误,不允许解引用void*
    ++pv; //错误,不允许对void*执行递增操作

    int* pi2 = static_cast<int*>(pv); // 显式转换回int*

    double* pd1 = pv; //错误
    double* pd2 = pi; //错误
    double* pd3 = static_cast<double*>(pv); //不安全
}

一般情况下,如果某个指针已经被转换成指向一种与实际所指对象类型完全不同的新类型,则使用转换后的指针是不安全的行为。
void最主要的用途是当我们无法假定对象的类型时,向函数传递指向该对象的指针;他用与从函数返回未知类型的对象.要使用这样的对象,必须先进行显式类型转换。

字面值常量nullptr表示空指针,即不指向任何对象的指针.我们可以将nullptr赋给其他任意指针类型,但是不能赋给其他内置类型:

1
2
3
int* pi = nullptr;
double* pd = nullptr;
int i = nullptr; // 错误,i不是指针

nullptr只有一个,它可以用于任意指针类型。

数组中元素的数量必须是常量表达式.如果你希望边界可变,最好使用vector:

1
2
3
4
5
voif f(int n)
{
    int v1[n]; //错误,数组的大小不是常量表达式
    vector<int> v2(n); //OK,包含n个int元素vector
}

C++允许静态的分配数组空间,也允许在栈上或在自由存储上分配数组空间:

1
2
3
4
5
6
int a1[10]; // 静态存储中的10个int

void f(){
    int a2[10]; // 栈上的20个int
    int* p = new int[40]; // 自由存储上的40个int
}

如果初始化器提供的元素数量不足,则系统自动把剩余的元素赋值为0:

1
2
3
int v5[8] = {1, 2, 3, 4};
等价于
int v5[] = {1, 2, 3, 4, 0, 0, 0, 0}

不允许用一个数组初始化另一个数组,因为数组不支持赋值操作:

1
2
int v6[8] = v5; //错误,不允许拷贝数组(不允许把int*赋给数组)
v6 = v5; //错误,不存在数组的赋值操作

同样,不允许以传值方式传递数组。
如果想给一组对象赋值,可以使用vector,array或valarry。可以只用字符串字面值常量初始化字符的数组。

字符串字面值常量是指双引号内的字符序列:“this is a string”
字符串字面值常量实际包含的字符数量比它表现出来的样子多一个,它以一个取值为0的空字符’\0’结尾。
字符串字面值常量的类型是"若干个const字符组成的数组"。
如果希望字符串能被修改,最好把字符放在一个非常量的数组中:

1
2
3
4
void f(){
    char p = "Zeno";
    p[0] = 'R'; //OK
}

当符号==作用于指针时,比较的是地址而非指针所指的值。

数组名可以看成是指向数组首元素的指针:

1
2
3
4
int v[] = {1,2,3,4};
int* p1 = v; //指向数组首元素的指针
int* p2 = &v[0]; //指向数组首元素的指针
int* p3 = v+4; //指向数组尾后元素的指针

令指针指向数组的最后一个元素的下一个位置是有效的,不过,因为该指针事实上指向的并不是数组中的任何一个元素,所以不能对它进行读写操作。试图获取和使用数组首元素之前或尾元素之后的地址都是未定义的行为,应尽量避免:

1
2
int* p4 = v-1;
int* p5 = v+7;

可以通过指向数组的指针加上一个索引值访问数组元素,也可以通过直接指向数组元素的指针进行访问:

1
2
3
4
5
6
7
8
9
void fi(char v[]){
    for(int i = 0; v[i] != 0; ++i)
        use(v[i]);
}

void fp(char v[]){
    for(char* p = v; *p != 0; ++p)
        use(*p);
}

前置*运算符执行解引用预算,因此p是指针p所指的字符,++元素按令p指向数组的下一个元素。
把+、-、++、–、等算数运算符用在指针上得到的结果依赖于指针所指对象的数据类型。当我们对T
类型的指针p执行算数运算时,p指向T类型的数组元素,p+1指向数组的下一个元素,p-1指向数组的上一个元素,上述规则意味着p+1对应的整数值比p对应的整数值大sizeof(T)。
指针的减法只有当参与运算的两个指针指向的是同一数组的元素时才有效,如果指针指向的位置既不是原数组中的元素,也不是尾后元素,那我们不能使用它,否则会产生未定义行为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void f(){
    int v1[10];
    int v2[10];

    int i1 = &v1[5]-&v1[3]; //i1 = 2
    int i2 = &v1[5]-&v2[3]; // 结果是未定义的

    int* p1 = v2+2; //p1=&v2[2]
    int* p2 = v2-2; //*p2是未定义的
}

因为数组的元素数量不一定能与数组本身存储在一起,所以数组不具有自解释性。当我们需要遍历一个数组,必须以某种方式提供元素的数量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void fp(char v[], int size)
{
    for(int i = 0; i != size; ++i)
        use(v[i]);
    for(int x:v)
        use(x); //错误,范围for循环对指针无效

    const int N = 7;
    char v2[N];
    for(int i = 0; i != N; ++i)
        use(v2[i]);
    for(int x:v2)
        use(x); // 当已知数组的大小时,可以使用范围for循环
}

不能以值传递的方式直接把数组传给函数,通常传递的是指向数组首元素的指针:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void comp(double arg[10])
{
    for(int i=0; i != 10; ++i)
        arg[i] += 99;
}

void f()
{
    double a1[10];
    double a2[5];
    double a3[100];

    comp(a1);
    comp(a2); //严重错误
    comp(a3); //只用到前十个元素
}

comp()函数的等价形式是:

1
2
3
4
5
void comp(double* arg)
{
    for(int i=0; i != 10; ++i)
        arg[i] += 99;
}

如果想在给函数传入一组元素的同时不丢掉数组的大小,就不能使用内置数组类型。可以将数组放在类中作为类的成员,或者直接定义一个句柄类。
使用内置数组有百弊而无一利。当需要一个接受二维矩阵的函数时,如果编译时知道数组的具体唯独当然没有问题。 数组的第一个维度与定位元素无关,只负责指明当前类型(int[5])包含几个元素(3),只要知道第二个维度sec,就能定义任意的ma[i][sec],此时,可以把数组的第一个唯独当成实参传入函数:

1
2
3
4
5
6
7
void print_mi5(int m[][5], int dim1)
{
    for(int i = 0; i != dim; i++){
        for(int j = 0; j != 5; j++)
            cout << m[i][j] << endl;
    }
}

但是当需要传入两个维度时,“显而易见的解决方案”并不有效:

1
2
3
4
5
6
7
void print_mij(int m[][], int dim1 int dim2)
{
    for(int i = 0; i != dim1; i++){
        for(int j = 0; j != dim2; j++)
            cout << m[i][j] << endl;
    }
}

因为多为数组的第二个维度必须是已知的,这样才能准确定位其中的元素。一种正确的解决方案是:

1
2
3
4
5
6
7
void print_mij(int* m, int dim1 int dim2)
{
    for(int i = 0; i != dim1; i++){
        for(int j = 0; j != dim2; j++)
            cout << m[i*dim2+j] << endl;
    }
}

要想使用该函数,我们只需传入一个代表矩阵的指针即可:

1
2
3
4
5
6
7
8
9

int test(){
    int v[3][5] = {
        {0,1,2,3,4},{10,11,12,13,14},{20,21,22,23,24}
    };
    print_m35(v);
    print_mi5(v,3);
    print_mij(&v[0][0],3,5);
}

在最后一个调用中使用v[0][0],此处使用v[0]也可以,因为它与v[0][0]等价,但是直接用传入v会引发类型错误。

很多对象的值一旦初始化就不会再改动:

  • 使用符号化常量的代码比直接使用字面值常量的代码更易维护。
  • 我们经常通过指针读取数据,但是很少通过指针写入数据。
  • 绝大多数函数的参数值负责读取数据,很少写入数据。

为了表达一经初始化就不可修改的特性,可以在对象的定义中加上const关键字:

1
2
3
cinst int model = 90
const int v[] = {1, 2, 3, 4};
const int x; //错误,缺少初始化器

一旦把某物声明成const,就确保它的值再作用域内不会发生改变:

1
2
3
4
void f(){
    model = 99; //错误
    v[2] = 8; //错误
}

使用const会改变一种类型,所谓的改变不是改变常量的分配方式,而是限制它的使用方式:

1
2
3
4
void g(const X* p)
{
    //此处无权修改*p
}

一个指针牵扯到两个对象:指针本身以及指针所指的对象。在指针的声明语句中“前置”const关键字将令所指的对象而非指针本身称为常量。要领指针本身成为常量,应该用*const代替普通的*:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void f1(char* p){
    char s[] = "Grom";

    const char* pc = s; //指向常量的指针
    pc[3] = 'g'; //错误,pc指向常量
    pc = p; //OK

    char *const cp = s; //常量指针
    cp[3] = 'a'; //OK
    cp = p; //错误,cp是一个常量

    const char *const cpc = s; // 指向常量的常量指针
    cpc[3] = 'a'; //错误:cpc指向常量
    cpc = p; //错误:cpc本身是一个常量
}

声明运算符*const的作用是令指针本身成为常量,出现在*前边的const是基本类型的一部分。

1
2
3
char *const cp; //指向char的常量指针
char const* pc; //指向常量const的指针
const char* pc2; //指向常量char的指针

从右向左的顺序读,例如,“cp是指向char的const指针”,而“pc2是指向const char的指针”。
对于同一个对象来说,通过一个指针访问它时是常量并不妨碍在其他情况下它是变量。这一点在涉及函数的实参时特别有用。可以把指针类型的实参声明成const,这样就能组织函数修改该指针所指的对象:

1
2
const char* strchr(const char* p, char c); //找到在字符串p中zifuc第一次出现的位置
char* strchr(char* p, char c); //找到在字符串p中zifuc第一次出现的位置

第一个函数的参数是常量字符串,函数无权修改其中的元素;它的返回值是指向const的指针,也不允许修改其所指的变量。第二个函数则没有这些限制。
C++允许把非const变量的地址赋给指向常量的指针,这样不会造成什么不可接受的后果。相反,常量的地址不能赋给某个不受限的指针,如果这样的话,用户有可能通过该指针修改对象的值:

1
2
3
4
5
6
7
8
void f4(){
    int a = 1;
    const int c = 2;
    const int* p1 = &c;
    const int* p2 = &a;
    int* p3 = &c; //错误,用const int*初始化int*
    *p3 = 7; //试图改变C
}

使用指针和使用对象名存在以下差别:

  • 语法形式不同,*p和p->m分别取代了obj和obj.m。
  • 同一个指针可以在不同时刻指向不同的对象。
  • 指针的值可能是nullptr,使用时要小心。

引用和指针类似,作为对象的别名存放对象的机器地址。与指针相比,不会带来额外的开销。与指针的区别主要包括:

  • 访问引用与访问对象本身从语法形式上看是一样的。
  • 引用所引的永远是一开始初始化的那个对象。
  • 不存在“空引用”,引用一定对应着某个对象。
    引用实际上是对象的别名,最重要的用途就是作为函数的实参或返回值。

为了体现左值/右值以及const/非const的区别,存在三种形式的引用:

  • 左值引用:引用那些我们希望改变值的对象。
  • const引用:引用那些我们不希望改变值的对象(如常量)。
  • 右值引用:所引用的值在使用之后就无须保留(如临时变量)。

着三种形式统称为引用,前两种自宁国市都是左值引用。

X&的意思是“X的引用”:

1
2
3
4
5
6
void f(){
    int var = 1;
    int& r {var};
    int x = r; //x =1 
    r = 2; //var = 2
}

为确保引用对应某个对象,必须初始化引用:

1
2
3
4
int var = 1;
int& r1 {var}; //OK
int& r2; //错误,缺少初始化
extern int& r3; //OK,r3在别处初始化

初始化引用和给引用赋值是完全不同的操作:

1
2
3
4
5
6
void g(){
    int var = 0; 
    int& rr{var};
    ++rr; //rr值加1
    int* pp = &rr; //pp指向var
}

++rr的含义不是递增引用rr,而是给rr所指的int(var)加1。引用本身的值一旦经过初始化就不能再改变了;他用冤兜指向一开始指定的对象。可以使用&rr得到一个指向rr所引对象的指针,但是不能令某个指针指向引用,也不能定义引用的数组。
引用的实现方式类似常量指针,每次引用实际上是对指针执行解引用。
当初始值是左值是,提供给“普通”T&的初始值必须是T类型的左值。
const T&的初始值不一定非得是左值,甚至可以不是T类型的:

1
2
double& dr = 1; //错误,此处需要左值
const double& cdr {1}; //OK

后一条语句可以理解为:

1
2
double temp = double{1}; //先用给定值创建临时变量
const double& cdr {temp}; //再将临时变量作为cdr的初始值

用于存放初始值的临时变量的生命周期从它创建之处开始,到它的作用域结束为止。
普通变量的引用和常量的引用必须分开来。
将引用作为函数的实参类型,这样函数就能修改传入其中的对象的值:

1
2
3
4
5
6
7
void increment(int& aa){
    ++aa;
}
void f(){
    int x = 1;
    increment(x); // x=2
}

尽量避免函数修改它的实参值,可以让函数显式的返回一个值达到同样地目的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int next(int p)
{
    return p+1;
}

void g(){
    int x = 1;
    increment(x); //x=2
    x = next(x); //x=3
}

除非函数名能明显的表达修改实参的意思,否则不要轻易使用“普通”引用。
引用还能作为函数返回值类型,此时,该函数既能作为赋值运算符的左侧运算对象,也能作为赋值运算符的右侧运算对象。

为支持对象的不同用法:

  • 非const左值引用所引的对象可以由用户写入内容。
  • const左值引用所引的对象从用户角度来看是不可修改的。
  • 右值引用对应一个临时对象,用户可以修改这个对象,并认定该对象以后不会用到了。

右值引用可以绑定到右值,但不能绑定到左值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
string var{"Cambridge"};
string f();

string& r1{var}; //左值引用
string& r2{f()}; //左值引用,错误,f()是右值
string& r3{"Princeton"}; //左值引用,错误,不允许绑定到临时变量
string&& rr1{f()}; //右值引用,正确,rr1绑定到一个右值
string&& rr2{var}; //右值引用,错误,var是左值
string&& rr3{"Oxford"}; //rr3医用一个临时变量

const string cr1&{"Harvard"}; //OK

声明符&&表示“右值引用”,const左值引用和右值引用都绑定右值,但是它们的目标完全不同:

  • 右值引用实现一种“破坏性读取”。
  • const左值引用的作用是保护参数内容不被修改。

右值引用使用方式基本等同于左值引用的方式:

1
2
3
4
5
string f(string&& s){
    if(s.size())
        s[0] = toupper(s[0]);
    return s;
}

永远是左值引用优先。
引用的引用指针只能作为别名的结果或者模板类型的参数。

指针与引用各有优势,也都存在不足:
如果需要更换所指对象,应该使用指针。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void fp(char* p){
    while(*p)
        cout << ++*p;
}

void fr(char& r){
    while(r)
        cout << ++r;//增加的是引用的char的值,不是引用本身
}

void fr2(char& r){
    char* p = *r;
    while(r)
        cout << ++r;
}

反之,如果想让某个名字永远对应同一对象,应该使用引用。 如果要自定义一个运算符,使之用于指向对象的某物,应该使用引用。

1
2
3
4
5
6
Matrix operator+(const Matrix&, const Matrix&)
Matrix operator-(const Matrix*, const Matrix*) //错误,不是用户自定义类型参数

Matrix y,z;
Matrix x = y + z;
Matrix x2 = &y - &z;

如果想让一个集合中的元素指向对象,应该使用指针。

1
2
3
4
5
int x,y;
string& a1[] = {x,y}; //错误,引用的数组
string* a2[] = {&x,&y}; 
vector<string&> s1 = {x,y}; //错误,引用的向量
vector<string*> s2 = {&x,&y}; 

如果需要表示值空缺,应该使用指针。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void fp(X* p){
    if(p == nullptr){
        //
    }
    else{

    }
}

void fr2(X& r){
    
}