要访问一个内存空间的值

可以用变量名,也可以用地址,地址就是指针类型的值

一、指针的定义

指针就是一种保存特定类型的地址的变量。

  1. static int i;
  2. static int* ptr = &i;

zhizheng1.png

  1. *ptr = 3;

zhizheng2.png

使用实例:

  1. int array[3] = {1,2,3};
  2. int *p;
  3. for(p = array; p < array + sizeof(array) / sizeof(int); ++p)
  4. {
  5. *p += 2;
  6. std::cout << *p << std::endl;
  7. }

运算符“和“&”

注意这是表达式中的运算符

  • 指针运算符:*
  • 取地址运算符:&

而声明中两者意义不同:

  • 声明指针: int a; (这个 应该是跟类型走的)
  • 声明引用: int &a = c; //引用必须绑定一个实际对象,并且一旦绑定不可解绑。

二、指针的初始化和赋值

在声明指针的时候之所以需要类型,是为了对指针取值,(计算机知道取几个字节内存),如果不需要取值可以用void类型。(也意味着不能取值)

另外,基于上述原理,有了类型之后可以进行指针的算术运算,而不至于取到一个值的中间字节,导致无效取值。

指针变量的初始化

  • 语法形式

存储类型 数据类型 *指针名=初始地址;

  • 例:int *pa = &a;

注意事项

  • 用变量地址作为初值时,该变量必须在指针初始化之前已声明过,且变量类型应与指针类型一致。
  • 可以用一个已有合法值的指针去初始化另一个指针变量。
  • 不要用一个内部非静态变量去初始化 static 指针。 (防止非静态量销毁,变成野指针)
  • 未赋初值的指针变量自动赋任意地址值

指针变量的赋值运算

  • 语法形式

指针名=地址(“地址”中存放的数据类型与指针类型必须相符)

必须是合法地址:

  • 向指针变量赋的值必须是地址常量或变量,不能是普通整数,例如:
    • 通过地址运算“&”求得已定义的变量和对象的起始地址
    • 动态内存分配成功时返回的地址
  • 例外:整数0可以赋给指针,表示空指针。用0或者NULL去表达空指针(C style)
    • C/C的NULL宏是个被有很多潜在BUG的宏。因为有的库把其定义成整数0,有的定义成 (void*)0。在C的时代还好。但是在C的时代,这就会引发很多问题。
    • C++11使用nullptr关键字,是表达更准确,类型安全的空指针
  • 允许定义或声明指向 void 类型的指针。该指针可以被赋予任何类型对象的地址。但不可取值。除非如示例代码一样进行显示转换。

例: void *general;

  1. #include <iostream>
  2. using namespace std;
  3. int main() {
  4. //!void voidObject; 错,不能声明void类型的变量
  5. void *pv; //对,可以声明void类型的指针
  6. int i = 5;
  7. pv = &i; //void类型指针指向整型变量
  8. int *pint = static_cast<int *>(pv); //void指针转换为int指针
  9. cout << "*pint = " << *pint << endl;
  10. return 0;
  11. }

三、常量指针与指针常量

常量指针 const type *

  • 不能通过指向常量的指针改变所指对象的值,但指针本身可以改变,可以指向另外的对象。
  1. int a;
  2. const int *p1 = &a; //p1是指向常量的指针
  3. int b;
  4. p1 = &b; //正确,p1本身的值可以改变
  5. *p1 = 1; //编译时出错,不能通过p1改变所指的对象

指针常量 type* const

  • 若声明指针常量,则指针本身的值不能被改变。
  1. int a;
  2. int * const p2 = &a;
  3. p2 = &b; //错误,p2是指针常量,值不能改变

四、指针的算术和关系运算

指针类型的算术运算

指针与整数的加减运算、指针++,—运算

  • 指针p加上或减去n
    • 其意义是指针当前指向位置的前方或后方第n个数据的起始位置。
  • 指针的++、—运算
    • 意义是指向下一个或前一个完整数据的起始。
  • 运算的结果值取决于指针指向的数据类型,总是指向一个完整数据的起始位置。
  • 当指针指向连续存储的同类型数据时,指针与整数的加减运和自增自减算才有意义。

指针与整数相加的意义

zzjj.png

指针类型的关系运算

  • 指向相同类型数据的指针之间可以进行各种关系运算。(一般用于数组前后关系)
    • 指向不同数据类型的指针,以及指针与一般整数变量之间的关系运算是无意义的。
  • 指针可以和 0 之间进行等于或不等于的关系运算。(判断是否是空指针)

例如:p==0或p!=0

五、指针与数组

数组名作为函数的参数时,则退化为一个指针

用指针访问数组 (一维数组)

数组是一组连续存储的同类型数据,可以通过指针的算术运算,使指针依次指向数组的各个元素,进而可以遍历数组。 数组变量就是常量地址,指向数组首元素地址。

  1. int a[10], *pa;
  2. pa = &a[0]; // 或 pa=a;

pa就是a[0],(pa+1)就是a[1],… ,*(pa+i)就是a[i]

a[i], (pa+i), (a+i), pa[i]都是等效的。

⚠不能写 a++,因为a是数组首地址、是常量。

指针数组(二维数组)

数组的元素是指针类型

指针 - 图4

  1. #include <iostream>
  2. using namespace std;
  3. int main() {
  4. int line1[] = { 1, 0, 0 }; //矩阵的第一行
  5. int line2[] = { 0, 1, 0 }; //矩阵的第二行
  6. int line3[] = { 0, 0, 1 }; //矩阵的第三行
  7. //定义整型指针数组并初始化
  8. int *pLine[3] = { line1, line2, line3 };
  9. cout << "Matrix test:" << endl;
  10. /* 等效于
  11. int pLine[][3] ={{ 1, 0, 0 },
  12. { 0, 1, 0 },
  13. { 0, 0, 1 }} ;
  14. */ //3不可省略
  15. //输出矩阵
  16. for (int i = 0; i < 3; i++) {
  17. for (int j = 0; j < 3; j++)
  18. cout << pLine[i][j] << " ";
  19. cout << endl;
  20. }
  21. return 0;
  22. }
  23. 输出结果为:
  24. Matrix test:
  25. 1,0,0
  26. 0,1,0
  27. 0,0,1

虽然用法等效

  1. std::cout<<*pLine[0]<<" val1--val2 "<<*pLine2[0]<<std::endl;
  2. std::cout<<*pLine<<" val1--val2 "<<*pLine2<<std::endl;
  3. 输出 ---------------------------
  4. 1 val1--val2 1
  5. 0x61fe0c val1--val2 0x61fda0

但是差别在于存储空间连续的问题:

1195562638818545664.png

int p[] V.S int (p)[]

前者是指针数组,后者是指向数组的指针。 定义涉及两个运算符:“”(间接引用)、“[]”(下标),“[]”的优先级别大于“”的优先级别。

前: 指针数组;是一个元素全为指针的数组.
后: 数组指针;可以直接理解是指针,只是这个指针类型不是int也不是char而是 int [4]类型的数组.(可以结合函数指针一并看看……)

  • int*p[4]———p是一个指针数组,每一个指向一个int型的指针
    • “[]”的优先级别高,所以它首先是个大小为4的数组,即p[4];剩下的“int *”作为补充说明,即说明该数组的每一个元素为指向一个整型类型的指针。
  • int (*q)[4]————-q是一个指针,指向int[4]的数组。
    • 它首先是个指针,即*q,剩下的“int [4]”作为补充说明,即说明指针q指向一个长度为4的数组。
    • q等同与一个二维数组的名称a,比如int a[m][n].

强制转换

  1. int A[m][n];
  2. int*a = (int*)A;//将A代表的int(*)[n],一个指向int[n]数组的指针
  3. //其实就是int[0][n]这个一维数组的名称,转为 int*指针。

六、指针与函数

指针作为函数参数

使用场景:

  • 数据双向传递(传入引用同效)
  • 传递一组数据,只传首地址运行效率高(数组)
    • 实参是数组名同时形参可以是指针
  1. #include <iostream>
  2. using namespace std;
  3. void splitFloat(float x, int *intPart, float *fracPart) {
  4. *intPart = static_cast<int>(x); //取x的整数部分
  5. *fracPart = x - *intPart; //取x的小数部分
  6. }
  7. int main() {
  8. cout << "Enter 3 float point numbers:" << endl;
  9. for(int i = 0; i < 3; i++) {
  10. float x, f;
  11. int n;
  12. cin >> x;
  13. splitFloat(x, &n, &f); //变量地址作为实参
  14. cout << "Integer Part = " << n << " Fraction Part = " << f << endl;
  15. }
  16. return 0;
  17. }
  18. //引用传参等效代码
  19. void splitFloat(float x, int &intPart, float &fracPart) {/*.....*/}
  20. float x, f;
  21. int n;
  22. splitFloat(x, n, f);
  1. //等效数组传参 注意const的使用
  2. #include <iostream>using namespace std;
  3. const int N = 6;
  4. void print(const int *p, int n);
  5. int main() {
  6. int array[N];
  7. for (int i = 0; i < N; i++)
  8. cin>>array[i];
  9. print(array, N);
  10. return 0;
  11. }
  12. void print(const int *p, int n) {
  13. cout << "{ " << *p;
  14. for (int i = 1; i < n; i++)
  15. cout << ", " << *(p+i);
  16. cout << " }" << endl;
  17. }

指针类型的函数

若函数的返回值是指针,该函数就是指针类型的函数。

  1. 返回类型 *函数名()
  2. { //函数体语句}

注意:

  • 不要返回非静态类型的局部变量——非法地址
  • 返回的函数要确保是在主调函数中有效、合法的地址
    • 比如返回主函数的数组某个元素的地址(查找)
    • 返回函数中new出来的动态内存,但是要注意释放。

函数指针

函数指针指向某种特定类型,函数的类型由其参数及返回类型共同决定,与函数名无关。举例如下:

  1. int add(int nLeft,int nRight);//函数定义

该函数类型为int(int,int),要想声明一个指向该类函数的指针,只需用指针替换函数名即可

  1. int (*pf)(int,int);//未初始化
  2. 或者
  3. typedef double (*PF)(int); // typedef 可以让原本是定义变量的表达式被变量名替代。相当于PF等待传入一个变量名,如下所示。
  4. PF pf;

则pf可指向int(int,int)类型的函数。pf前面有*,说明pf是指针,右侧是形参列表,表示pf指向的是函数,左侧为int,说明pf指向的函数返回值为int。则pf可指向int(int,int)类型的函数。而add类型为int(int,int),则pf可指向add函数。

  1. pf = add;//通过赋值使得函数指针指向某具体函数
  2. val = (*pf)(3,10); // 使用*pf调用add函数

注意:*pf两端的括号必不可少,否则若为如下定义:

  1. int *pf(int,int);//此时pf是一个返回值为int*的函数,而非函数指针

编译时系统就会为函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址。函数名表示的就是这个地址。既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针。指向函数的指针变量没有 ++ 和 — 运算。(类型原因)

函数指针的使用

举个例子:

  1. int Func(int x); /*声明一个函数*/
  2. int (*p) (int x); /*定义一个函数指针*/
  3. p = Func; /*将Func函数的首地址赋给指针变量p*/
  4. int a ,b = 5; a = p(b); //使用p等价于调用Func,亦可用(*p)(b)。因为函数都是通过地址调用。

赋值时函数 Func 不带括号,也不带参数。由于函数名 Func 代表函数的首地址,因此经过赋值以后,指针变量 p 就指向函数 Func() 代码的首地址了。(类似数组名的道理)

标准C函数指针

1函数指针的定义

1.1 普通函数指针定义
  1. int (*pf)(int,int);

1.2 使用typedef定义函数指针类型

typedef的用法其实就是声明一个变量A,然后这个变量名A变成了一种类型,以后可以用它去声明别的变量a,相当于把别的变量a替换到typedef句子中的A的位置。

  1. typedef int (*PF)(int,int);
  2. PF pf;//此时,为指向某种类型函数的函数指针类型,而不是具体指针,用它可定义具体指针

2函数指针的普通使用
  1. pf = add;
  2. pf(100,100);//与其指向的函数用法无异
  3. (*pf)(100,100);//此处*pf两端括号必不可少

注意:add类型必须与pf可指向的函数类型完全匹配

3函数指针作为形参
  1. //第二个形参为函数类型,会自动转换为指向此类函数的指针
  2. Void fuc(int nValue,int pf(int,int));
  3. //等价的声明,显示的将形参定义为指向函数的指针
  4. Void fuc(int nValue,int (*pf)(int,int));
  5. Void fuc(int nValue,PF);

形参中有函数指针的函数调用,以fuc为例:

  1. pf = add;//pf是函数指针
  2. fuc(1,add);//add自动转换为函数指针
  3. fuc(1,pf);

4返回指向函数的指针

4.1 使用typedef定义的函数指针类型作为返回参数
  1. PF fuc2(int);//PF为函数指针类型

4.2 直接定义函数指针作为返回参数
  1. int (*fuc2(int))(int,int);//显示定义 ,*号不可省略(指针类型的函数的要求)

说明:按照有内向外的顺序阅读此声明语句。fuc2有形参列表,则fuc2是一个函数,其形参为fuc2(int),fuc2前面有*,所以fuc2返回一个指针,指针本身也包含形参列表(int,int),因此指针指向函数,该函数的返回值为int.

总结:fuc2是一个函数,形参为(int),返回一个指向int(int,int)的函数指针。

二 C++函数指针

1由于C完全兼容C,则C中可用的函数指针用法皆可用于C

2 C++其他函数(指针)定义方式及使用

2.1 typedef与decltype组合定义函数类型
  1. typedef decltype(add) add2;

decltype返回函数类型,add2是与add相同类型的函数,不同的是add2是类型,而非具体函数。

使用方法:

  1. add2* pf;//pf指向add类型的函数指针,未初始化

2.2 typedef与decltype组合定义函数指针类型
  1. typedef decltype(add)* PF2;//PF2与1.1PF意义相同
  2. PF2 pf;// pf指向int(int,int)类型的函数指针,未初始化

2.3 使用推断类型关键字auto定义函数类型和函数指针

  1. auto pf = add;//pf可认为是add的别名(个人理解)
  2. auto *pf = add;//pf为指向add的指针

3函数指针形参(注意自动转化)
  1. typedef decltype(add) add2;
  2. typedef decltype(add)* PF2;
  3. void fuc2 (add2 add);//函数类型形参,调用自动转换为函数指针
  4. void fuc2 (PF2 add);//函数指针类型形参,传入对应函数(指针)即可

说明:不论形参声明的是函数类型:void fuc2 (add2 add);还是函数指针类型void fuc2 (PF2 add);都可作为函数指针形参声明,在参数传入时,若传入函数名,则将其自动转换为函数指针。

4 返回指向函数的指针

4.1 使用auto关键字
  1. auto fuc2(int)-> int(*)(int,int) //fuc2返回函数指针为int(*)(int,int)

4.2 使用decltype关键字
  1. decltype(add)* fuc2(int)//明确知道返回哪个函数,可用decltype关键字推断其函数类型,

5 成员函数指针

5.1普通成员函数指针使用举例
  1. class A//定义类A
  2. {
  3. private:
  4. int add(int nLeft, int nRight)
  5. {
  6. return (nLeft + nRight);
  7. }
  8. public:
  9. void fuc()
  10. {
  11. printf("Hello world\n");
  12. }
  13. };
  14. typedef void(A::*PF1)();//指针名前需加上类名限定
  15. PF1 pf1 = &A::fuc; //必须有&
  16. A a;//成员函数地址解引用必须附驻与某个对象地址,所以必须创建一个队形
  17. (a.*pf1)();//使用成员函数指针调用函数

5.2继承中的函数指针使用举例
  1. class A
  2. {
  3. public:
  4. void fuc()
  5. {
  6. printf("Hello fuc()\n");
  7. }
  8. void fuc2()
  9. {
  10. printf("Hello A::fuc2()\n");
  11. }
  12. };
  13. class B:public A
  14. {
  15. public:
  16. virtual void fuc2()
  17. {
  18. printf("Hello B::fuc2()\n");
  19. }
  20. };
  21. typedef void(A::*PF1)();
  22. typedef void(B::*PF2)();
  23. PF1 pf1 = &A::fuc;
  24. int main()
  25. {
  26. A a;
  27. B b;
  28. (a.*pf1)(); //调用A::fuc
  29. (b.*pf1)(); //调用A::fuc
  30. pf1 = &A::fuc2;
  31. (a.*pf1)(); //调用A::fuc2
  32. (b.*pf1)(); //调用A::fuc2
  33. PF2 pf2 = &A::fuc2;
  34. (b.*pf2)(); //调用A::fuc2
  35. }

6重载函数的指针

6.1 重载函数fuc
  1. void fuc();
  2. void fuc(int);

6.2 重载函数的函数指针
  1. void (*PF)(int) = fuc;//PF指向fuc(int)
  2. int(*pf2)(int) = fuc;//错误没有匹配的类型

注意:编译器通过指针类型决定选取那个函数,指针类型必须与重载函数中的一个精确匹配。

七、指针与对象

定义形式

  1. 类名 *对象指针名;
  1. Point a(5,10);
  2. Piont *ptr;
  3. ptr=&a;

通过指针访问对象成员

对象指针名->成员名

例:ptr->getx() 相当于 (*ptr).getx();

this指针

指向当前对象自己

  • 隐含于类的每一个非静态成员函数中。指出成员函数所操作的对象。
    • 当通过一个对象调用成员函数时,系统先将该对象的地址赋给this指针,然后调用成员函数,成员函数对对象的数据成员进行操作时,就隐含使用了this指针。
  • 例如:Point类的getX函数中的语句:
    return x;
    相当于:
    return this->x;

八、动态内存分配

C++程序的内存格局:

指针 - 图6 根据这个解释,我们可以得知在类的定义时,类成员函数是被放在代码区,而类的静态成员变量在类定义时就已经在全局数据区分配了内存,因而它是属于的。对于非静态成员变量,我们是在类的实例化过程中(构造对象)才在栈区或者堆区为其分配内存,是为每个对象生成一个拷贝,所以它是属于对象的。

  1. void f() { int* p=new int[5]; }

这条短短的一句话就包含了堆与栈,看到new,我们首先就应该想到,我们分配了一块堆内存,那么指针p呢?他分配的是一块栈内存,所以这句话的意思就是:在栈内存中存放了一个指向一块堆内存的指针p。在程序会先确定在堆中分配内存的大小,然后调用operator new分配内存,然后返回这块内存的首地址,放入栈中,他在VC6下的汇编代码如下:

  1. 00401028 push 14h
  2. 0040102A call operator new (00401060)
  3. 0040102F add esp,4
  4. 00401032 mov dword ptr [ebp-8],eax
  5. 00401035 mov eax,dword ptr [ebp-8]
  6. 00401038 mov dword ptr [ebp-4],eax

这里,我们为了简单并没有释放内存,那么该怎么去释放呢?是delete p么?澳,错了,应该是delete []p,这是为了告诉编译器:我删除的是一个数组,VC6就会根据相应的Cookie信息去进行释放内存的工作。

动态申请内存操作符 new (只能用指针访问)

  • new 类型名T(初始化参数列表)
  • 功能:在程序执行期间,申请用于存放T类型对象的内存空间,并依初值列表赋以初值。
  • 结果值:成功:T类型的指针,指向新分配的内存;失败:抛出异常。

释放内存操作符 delete

  • delete 指针p
  • 功能:释放指针p所指向的内存。p必须是new操作的返回值。

这里,我们为了简单并没有释放内存,那么该怎么去释放呢?是delete p么?澳,错了,应该是delete []p,这是为了告诉编译器:我删除的是一个数组,VC6就会根据相应的Cookie信息去进行释放内存的工作。

九、智能指针

  • unique_ptr :不允许多个指针共享资源,可以用标准库中的move函数转移指针
  • shared_ptr :多个指针共享资源,当计数器(共享数)为0时自动回收内存。
  • weak_ptr :可复制shared_ptr,但其构造或者释放对资源不产生影响

十、对象复制与移动

左值和右值

左值和右值都是针对表达式而言的,左值是指表达式结束后依然存在的持久对象,右值指表达式结束时就不再存在的临时对象(将亡值)——显然右值不可以被取地址。

move函数可以将一个左值变成右值。

浅层复制与深层复制

  • 浅层复制:实现对象间数据元素的一一对应复制。(合成默认复制构造函数)
  • 深层复制:当被复制的对象数据成员是指针类型时,不是复制该指针成员本身,而是将指针所指对象进行复制。c ntNum(const IntNum & n) : xptr(new int(*n.xptr)){}//复制构造函数 ~IntNum(){delete xptr;} //析构函数

移动构造

在现实中有很多这样的例子,我们将钱从一个账号转移到另一个账号,将手机SIM卡转移到另一台手机,将文件从一个位置剪切到另一个位置……移动构造可以减少不必要的复制,带来性能上的提升。

  • C++11标准中提供了一种新的构造方法——移动构造。
  • C++11之前,如果要将源对象的状态转移到目标对象只能通过复制。在某些情况下,我们没有必要复制对象——只需要移动它们。
  • C++11引入移动语义:
    • 源对象资源的控制权全部交给目标对象

问题与解决

  • 当临时对象在被复制后,就不再被利用了(比如说函数返回的对象)。我们完全可以把临时对象的资源直接移动,这样就避免了多余的复制操作。

yidonggouzao.png

移动构造

  • 什么时候该触发移动构造?
    • 有可被利用的临时对象
  • 移动构造函数:(右值引用)
  1. class_name ( class_name && ) ;

例子:

  1. IntNum(IntNum && n): xptr(n.xptr){ //移动构造函数
  2. n.xptr = nullptr;}

拷贝构造函数中,对于指针,我们一定要采用深层复制,而移动构造函数中,对于指针,我们采用浅层复制。