# C++基础3-指针

本文是《C++ Primer Plus》的笔记,本文中的案例均自己实践过,如需转发请在转发开头贴上原文地址,谢谢!

# 指针

# 定义

作为C语言中的精华部分,有必要把这个类型单独拿出来,什么是指针?我们把数据在内存中存储想象成是在柜子中存钱,假如钱比较少我们直接放到柜子中的抽屉里,如果钱很多,我们可以把钱放在一个秘密的地方,然后把这个地方放在抽屉中。其他变量都是直接存放数据,而指针变量中存放的是指向存放该数据的内存地址。

//定义格式
基类型 * 指针变量名;
//实例:
    int a = 100, b = 10;
    int*pa = &a;
    int *pb = &b;
    int * pc = &a;
    cout << *pa << endl;
    cout << * pb << endl;
1
2
3
4
5
6
7
8
9

从定义看出一个指针至少包含两个方面:

  1. 数据存放的内存地址。
  2. 存放数据的基类型。因为每种数据类型所占用的内存大小也是不一样的,指针移动一个位置经历的内存位移也是不一样的。

# 相关运算符

&:取变量地址运算符。
*:指针运算符,取指针所指向的内存中的内容。

int a;
int * p1 = &a;

//由于*和自加运算符都是相同的运算优先级,注意理解下面几个等式。*p1)++;
*p1++;
++ *p1;
* ++p1;

//先进行*p1运算得到a,再&a得到a的地址,也就是p1。
&*p1;
//先进行&a,得到p1再进行*p1运算得到a。
*&a;


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  1. 从字面意上理解有下面这两个等式:p1=&a和*p1=a。
  2. &,*,和自加自减运算符都是相同的优先级,运算顺序从右到左。

# 显示指针的地址

对于每个变量,使用(void *)转换输出的话会把变量的内容的内存表示以十六进制输出来,对于指针类型,会输出对应的内存地址。

    cout << "正常输出指针地址:" << endl;
    int a = 32;
    cout << &a << endl;

    int * aPtr = &a;
    cout << aPtr << endl;
    cout << (void *)((char *)a) << endl;
    //(char *)不会将数字转换为相应的字符,而是传输内存中存储的位表示。但是cout会将每个字节作为ASCII码进行解释,这就会导致乱码。

    int c[3] = { 1,2,3 };
    cout << c << endl;

    cout << "---------------" << endl;
    //显示输出字符指针的指向地址

    const char * b = "hi";
    cout << b << endl;
    cout << (void *)b << endl;

    cout << "---------------" << endl;

    char d[10] = "hi,world";
    char e[10] = "hehe";
    char * f1 = d;

    cout << f1 << endl;
    cout << (void *)f1 << endl;
    cout << &f1 << endl;

    f1 = e;
    cout << f1 << endl;
    cout << (void *)f1 << endl;
    cout << &f1 << endl;
    //f1自身变量的地址不会改变,但是f1指向的地址会变,因为它原来指向d,后来指向e。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# 指针应用

# 整型与指针

int * pt;
//pt = 0xB8000000; //这句是错误的因为指针与整型是两种类型。
pt = (int *)0xB8000000;
1
2
3

# 指针作为函数参数

通过引用传递,可以改变被引用变量的值。

void swap(int *a, int *b) {
    int temp;
    temp = *a;
    *a = *b;
    *b = temp;
}

    int a = 1, b = 2;
    swap(&a, &b);
    cout << a << " " << b << endl;
1
2
3
4
5
6
7
8
9
10

# 数组指针

一个指针可以指向变量,也可以指向一个数组,首先它是一个指针,指向一个数组。

int * p;
//可以指向一维整型数组
int (*p)[4];
//可以指向二维,每行为4个整型元素的数组。
1
2
3
4

引用数组元素可以用下标法也可以用指针法,基本上是形式不一样。

    int arr[] = {1,2,3,4,5,6};
    int * pointerArr,* pointerArr2;

    //数组名默认是数组第一个元素的地址
    pointerArr = arr;
    pointerArr2 = &arr[0];

    cout << pointerArr[2] << " " << arr[2] << " " << pointerArr2[2] <<endl;
    cout << *(pointerArr+2) << " " << *(arr+2) << " " << *(pointerArr2+2) <<endl;
1
2
3
4
5
6
7
8
9

# 多维数组指针

    int intArr[2][3] = {
        {1,2,3},
        {4,5,6},
    };

    int * intPtr;
    intPtr = intArr[0];

    cout << sizeof(intArr)/sizeof(intArr[0][0]) << endl;
    cout << "------------使用指向整型的指针遍历数组-----------------" << endl;

    int i = 0;
    while (i<6) {
        cout << *intPtr++ << endl;
        i++;
    }
    cout << "------------使用指向3个整型元素的数组指针来遍历数组--------------" << endl;

    int (*intRowPtr)[3];
    intRowPtr = intArr;

    i = 0;
    while (i < 2) {
        cout << **intRowPtr << endl;
        intRowPtr++;
        i++;
    }
    cout << "---------------------------------" << endl;

    i = 0;
    intRowPtr = intArr;
    while (i < 6) {
        cout << *(*intArr+i) << endl;
        cout << *(*intRowPtr+i) << endl;
        i++;
    }
    cout << "-------------获取0行1列元素--------------" << endl;

    cout << intArr[0][1] <<endl;
    cout << intRowPtr[0][1] <<endl;
    cout << *(*(intArr+0)+1) <<endl;
    cout << *(*(intRowPtr+0)+1) <<endl;
    cout << *(intRowPtr[0]+1) <<endl;

    cout << "-------------获取0行地址--------------" << endl;
    cout << intArr << " " << *(intArr + 0) << " " << intRowPtr[0] << " " << *intArr <<endl;
    cout << "虽然都获取的是0行首地址,但有的是指向行,有的是指向列,如果移动下指针,指向行是移动12byte,指向列是移动4byte" << endl;
    cout << intArr+1 << " " << *(intArr + 0)+1 << " "<< intRowPtr[0]+1 << " "<< *intArr+1 <<endl;
    cout << "-------------获取第1行地址--------------" << endl;
    cout << intArr+1 << " " << intRowPtr[1] << " " << &intRowPtr[1] << " " << *(intRowPtr+1) <<endl;
    cout << "虽然都获取的是1行首地址,但有的是指向行,有的是指向列,如果移动下指针,指向行是移动12byte,指向列是移动4byte" << endl;
    cout << intArr+2 << " " << intRowPtr[1]+1 << " " << &intRowPtr[1] + 1 << " " << *(intRowPtr + 1)+1 <<endl;
    cout << "-------------获取第1行第1列地址--------------" << endl;
    cout << *(intArr +1)+1 << " " << &intRowPtr[1][1] <<endl;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

# 数组的地址

    int arr[] = { 1,2,3,4,5,6 };

    cout << "arr:" << (void *)arr << "    arr+1:" << (void *)(arr + 1) << "    &arr:" << (void *)(&arr) << "    &arr+1:" << (void *)(&arr + 1) << endl;

    int arr1[2][2] = {
        {1,2},
        {3,4}
    };
    cout << "arr1:" << (void *)arr1 << "    arr1+1:" << (void *)(arr1 + 1) << "    &arr1:" << (void *)(&arr1) << "    &arr1+1:" << (void *)(&arr1 + 1) << endl;
1
2
3
4
5
6
7
8
9

虽然 数组名 和 &数组名 都是一样的地址,但:

  1. 一维数组中,数组名 指向的是第一个元素的地址。数组名+1 指向的是数组中第二个元素的地址。&数组名 是这个数组的地址,&数组名+1 指向的是这个数组末尾后的开始地址。
  2. 二维数组中,数组名 指向的是第一行元素的首地址。数组名+1 指向的是数组中第二行元素的首地址。&数组名 是这个数组的地址,&数组名+1 指向的是这个数组末尾后的开始地址。

# 字符串指针

    char name[20]="hello world";
    char name2[20]="hi,world";
       //常量指针,其所指向的对象是常量
    char * namePtr2 =name2;
    // char * namePtr2 ="hi"; //error,const char * 不能用于初始化char * 类型实体

    strcpy_s(name, "hello world2");
    namePtr = "Hi World!3";

    cout << "----------注意输出字符数组和字符指针的区别---------" << endl;
    cout << name << endl;
    cout << namePtr << endl;
    //char * const namePtr2 = "hi"; //error,不能将const char * 赋值给char * const
    char * const namePtr3 = name2; //指针常量,指针自身值不能改变。指向地址不能改变。
    //namePtr3 = name; //error,不能修改指针w。
    const char * const namePtr4 = namePtr2;
    cout << namePtr4 << endl;
    const char * const namePtr5 = "hi,hello,world"; //指针指向地址不能改变,且所指向对象必须是常量。
    //namePtr4 = "this"; //error
    namePtr2 = name;
    cout <<namePtr4 << endl;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

const char * str1 常量指针,指向常量的指针,限制了通过指针来修改它指向的值,但是可以修改str1指向其他地址。
char * const str2 指针常量,本身是一个常量,这个常量是一个指针,这个指针不能再被赋给其他值,但是他所指向的内容可以被修改。
const char * const str3 这个指针不能改变,它所指向的值也不能改变。

具体看如下事例:

    int a = 1, b = 2;
    const int * pt1;
    int const * pt2;
    int * const pt3 = &a;
    pt1 = &a;
    //*pt1 = 3; //无效
    pt1 = &b;
    cout << *pt1 << endl;
    pt2 = &a;
    pt2 = &b;
    cout << *pt2 << endl;
    *pt3 = 4;
    //pt3 = &b; 无效;
    cout << *pt3 << endl;

    const int * pt4 = pt1;
    cout << *pt4 << endl;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 函数与指针

//指针函数
char * MyPrint(char * input) {
    cout << input << endl;
    return input;
}

    char outStr[] = "hello world";
    //函数指针
    char * (*MyPrintPointer)(char * input);
    MyPrintPointer = MyPrint;
    cout << (*MyPrintPointer)(outStr) << endl;
    cout << MyPrintPointer(outStr) << endl;
1
2
3
4
5
6
7
8
9
10
11
12

从上面会发现*MyPrintPointer和MyPrintPointer都可以调用函数,这是有历史原因的:一种观点认为它是函数指针,而*MyPrintPointer才是函数应该使用(*MyPrintPointer)()用作函数调用。而另一种观点认为,函数名是指向该函数的指针,指向函数的指针的行为应该至少与函数名相似,因此可以直接使用MyPrintPointer()。C++将这两种都实现了,都可以使用。

指针函数:类型名 * 函数名(参数表)
函数指针:类型名 (*)(参数表)

函数指针是一个指针,指向一个函数,可以使用这个指针来调用函数。参数表中可不用写形参名,可以将函数作为一个参数传递到其他函数。
指针函数是一个函数,返回一个指针。

# 指针数组和二重指针

类型名 * 数组名[数组长度]
类型名 ** 指针名

它是一个数组,其中的元素为指针。二重指针可以指向指针数组。

//函数指针
char ** MyPrint(char * input[]) {
    cout << input << endl;
    return input;
}

    char a[10] = "hi";
    char b[10] = ",";
    char c[10] = "xie";

    char * outStr[3];
    outStr[0] = a;
    outStr[1] = b;
    outStr[2] = c;
    char ** (*MyPrintPointer)(char *[]);
    MyPrintPointer = MyPrint;

    char ** tempStr= (*MyPrintPointer)(outStr);
    for (int i = 0; i < 3; i++)
    {
        cout << *tempStr << endl;
        tempStr++;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 结构体与指针

struct student2 {
    int age;
}stu1;

    stu1.age = 10;
    cout << stu1.age << endl;
    student2 * pstu1 = &stu1;
    pstu1->age = 20;
    cout << stu1.age << endl;
    (*pstu1).age = 30;
    cout << stu1.age << endl;
1
2
3
4
5
6
7
8
9
10
11

如果一个指针pstu1指向一个结构体stu1,引用结构体中的值有如下三种形式:

  1. stu1.num
  2. pstu1->num
  3. (*pstu1).num

# 指针空间分配

c-style 空间分配

    cout << sizeof(int) << endl;
    int *readPtr=(int *)malloc(16);
    for (int i = 0; i < 4; i++) {
        *(readPtr+i) = i;
    }
    int *readPtr2 = (int *)calloc(4,4);
    for (int i = 0; i < 4; i++) {
        *(readPtr2 + i) = i;
    }
    //free(readPtr);
    readPtr = (int *)realloc(readPtr,24);
    *(readPtr + 4) = 10;
    *(readPtr + 5) = 11;
    for (int i = 0; i < 6; i++) {
        cout << *(readPtr+i) <<endl;
        cout << *(readPtr2+i) <<endl;
    }

    void *readPtrVoid = (void *)malloc(16);
    //readPtrVoid+1 void 类型无法进行+ -操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

c++ 空间分配

    int * intptr = new int;
    int * intArrPtr = new int[2];
    (*intptr) = 100;
    cout << (*intptr) << endl;;
    for(int i=0;i<2;i++){
        *(intArrPtr + i) = i;
        cout << *(intArrPtr + i) << endl;;
    }
    delete intptr;
    //delete [] intptr;
    delete [] intArrPtr;
    //delete intArrPtr;
    //只能释放一次内存
1
2
3
4
5
6
7
8
9
10
11
12
13

c++中有三种管理数据内存的方式:

  1. 自动存储:在函数内部定义的常规变量使用这部分空间,自动变量是一个局部变量,其作用于为包含它的代码块。这部分空间存储在栈中,执行代码时依次压入栈,执行完按相反顺序退栈。
  2. 静态存储:在整个程序执行期间都存在的存储方式。定义这种变量有两种方式:
    1. 在函数外面定义
    2. 在声明变量时使用关键字static
  3. 动态存储:使用new delete来管理一个内存池,这部分变量存储在堆(heap)。这使得能够在一个函数中分配内存,在另一个函数中释放,数据的声明周期完全不受程序或函数的生存时间控制。

内存泄漏:

使用new在堆上创建变量后,没有调用delete,即使指向该内存的指针由于作用域和对象生命周期的原因而被释放,在堆上实际分配的空间依旧存在,这部分空间就是被泄漏的空间,他们无法被回收,无法被使用,在程序的整个生命周期内都不可使用。

C99允许使用基类型为void(typeless pointer 无类型指针)的指针类型。不应该把void理解为任何的类型,而应理解为空类型不确定类型

# 指针小结

# 正确区分和指针相关的名称

比如 数组指针和指针数组,我们要区分这两个概念记得后面的标识这个东西的根本性质,前面的都是都是定语,做修饰用。比如,数组指针是指针,指向一个数组。指针数组是一个数组,每个元素是指针。

# 引用

# 定义格式

类型标识符 &引用名=目标变量名; const 类型标识符 &引用名=目标变量名;

下面是常量引用,即不能根据引用来改变值。

# 引用理解

引用是一个变量或函数的别名,对引用的操作与对变量直接操作完全一样。注意:

  1. 引用仅是变量别名,而不是实实在在定义的一个变量,引用本身并不占用内存,而是和目标变量共同指向目标变量的内存地址。
  2. 取地址符&不再表示取变量的地址,只是标识引用变量。
  3. 定义一个引用时,必须对其初始化。
    int a = 1;
    int &b = a;
    int& b1=a;
    int & b2=a;
    b = 2;
    a = 3;
    cout << a << " " << b << " " << b1 << " " << b2 << endl;
1
2
3
4
5
6
7

# 引用的应用

# 作为函数的形参

void swap(int &a, int &b) {
    int temp = a;
    a = b;
    b = temp;
}

void swapPtr(int *a,int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

void main(){
    int a = 1, b=2;
    swap(a, b);
    cout << a << " " << b << endl;
    swapPtr(&a, &b);
    cout << a << " " << b << endl;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

不管从形参引用和实参调用上看都简化不少。

# 引用作为形参的特别之处

double refVar(const int &x) {
    return x+2;
}

int main(int argc,char *argv[])
{
    int a = 1;
    long b = 2;

    refVar(b); //不用const修饰时会提示 无法用long类型去初始化int &类型的变量。
    refVar(a+2); //不用const修饰时会提示 提示:非常量引用的初始值必须为左值。
    cout << refVar(a) << endl;
    cout << refVar(b) << endl;
    cout << refVar(a+2) << endl;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

何时使用引用参数?

  1. 能够修改调用函数中的数据对象。
  2. 通过传递引用而不是整个数据对象,来提高程序运行速度。

当引用参数被const修饰时,在下列情况会生成临时变量:

  1. 实参类型正确,但不是左值。
  2. 实参类型不正确,但可以转换为正确的类型。

为何会有const的引用变量形参呢?

既然是引用变量为啥要加上const不让修改呢?我觉得像整型浮点型或者占用空间较小的类型,直接使用普通值传递即可,而像一些数组等占用空间较大的变量使用引用变量类型则可以避免复制值,且只使用不修改的效果。

为什么要尽量的使用const?

  1. 可以避免无意修改数据。
  2. const能够处理const和非const实参,否则只能接受非const。
  3. const类型的引用使函数能够正确生成并使用临时变量。

# 引用作为返回值

float a = 1;

float & returnGlobalValue(float &a) {
    return a;
}

class classA {
public :
    int a = 0;

    classA & echoA() {
        cout << a++ << endl;
        return *this;
    }

};

int main()
{
    float &b = returnGlobalValue(a);
    float c = returnGlobalValue(a);

    c = 2;
    cout << a << " " << b << " " << c << endl;

    b = 3;
    cout << a << " " << b << " " << c << endl;


    classA tempA = classA();
    tempA.echoA().echoA().echoA();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

输出为:

1 1 2
3 3 2
0
1
2
1
2
3
4
5

注意:

  1. 函数返回引用值不能返回局部变量。
  2. 可以用来自身对象来实现链式调用。

# 引用指针与指针引用

看这个小标题会不会比较混乱呢,:)先不要慌,先使用我们之前的名词分析法,前面是修饰,后面是主语,引用指针是一个指针,它指向一个引用;指针引用是一个引用,它指向一个指针。

为什么会出现这两个概念呢?

# 问题背景

void CopyCharArray(char * tName, const char * nameValue)
{
    int len = std::strlen(nameValue) + 1;
    tName = new char[len];
    strcpy_s(tName, len, nameValue);
}

int main(int argc,char *argv[])
{
    char * name=new char[3];
    name[0] = 'h';
    name[1] = 'i';
    name[2] = '\0';
    cout << name << endl;
    CopyCharArray(name, "hello,world");
    cout << name << endl;
}

程序会输出:
hi
hi

而我们期待输出:
hi
hello,world

为什么会出现这个问题呢?想想之前我们用指针做形参改变其指向内存空间的值。

void ChangeIntVariable(int * tInt, int value) {
    *tInt = value;
}

int main(int argc,char *argv[])
{

    int a = 2;
    int *aP = &a;
    cout << a << endl;
    ChangeIntVariable(aP, 3);
    cout << a << endl;
}

程序会输出
2
3

和期望的一样。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

这两种方式有什么区别呢?我们来分析下:

指针和引用

在改变name时,tName指向name所指向的内存空间,而后面tName指向了新开辟的内存空间,而name依旧指向原来的内存空间;而改变aP时,aP和tInt始终指向a的内存空间。所以如果我们想改变name的值实质是要改变name指向的内存空间。我们应该让tName指向name,而不是指向name所指向的内存空间。name是一个指针,而我们要引用它,这就是指针引用的用途。我们要想让上面的name改变,需要把char * tName改变为 char * & tName。

注意引用是一个对象的别名,它不是一种数据类型,它自身没有分配内存空间,没有地址,引用需要被初始化为能转换成所引用类型的对象,引用本身不能再被引用。引用没有地址,那怎么用指针指向它呢?所以引用指针是不存在的,下面这种形式是错误的。

    int a = 10;
    int & * aR = &a;
1
2

# 左值与右值

# 含义

  1. 左值是能对其取地址的量;右值是不能对其取地址的量。
  2. 左值位于赋值运算符的左边,它在内存中必须有实体;右值位于赋值运算符的右边,可以存在于内存或者CPU寄存器当中。
  3. 左值参数是可被引用的数据对象,例如变量,数组元素,结构成员,引用和指针。右值一般是临时变量,匿名对象,字面常量。
  4. const与左右值无关,一般能被const修饰的只能是左值,也有例外,比如const函数返回值就是返回了一个临时变量。
  5. 一个左值表达式代表的是对象本身,而右值表达式代表的是对象的值。

# 左值引用,右值引用

上面说的常规引用都是左值引用。右值也是可以有右值引用的。注意右值一般被const指针指向,也就是const lvalue绑定到一个rvalue上,但是这种操作不能改变引用的对象,这时候右值引用就变得有意义了。比如

int main(int argc,char *argv[])
{

    int && b = 3;
    cout << &b << endl;
    b = 4;
    cout << &b << endl;
}
1
2
3
4
5
6
7
8

右值引用是为了move,这个是什么?后面再看了。