彻底学会C++指针,引用

640K ought to be enough for everybody. ——Bill Gates

1.指针的概念

计算机体系中的存储层次
在这里插入图片描述

C++中内存单元内容与地址

  • 内存由很多内存单元组成。这些内存单元用于存放各种类型的数据。
  • 计算机对内存的每个内存单元都进行了编号,这个编号就称为内存地址,地址决定了内存单元在内存中的位置。
  • 记住这些内容单元地址不方便,于是C++语言的编译器让我们通过名字来访问这些内存位置。

举例:

int a = 112,b = -1;
float c = 3.14;
int* d = &a;
float* e = &c;

在这里插入图片描述

指针的定义和间接访问操作

  • 指针定义的基本形式:指针本身就是一个变量,其符合变量定义的基本形式,它存储的是值的地址。对类型T,T*是"到T的指针"类型,一个类型为T*的变量能保存一个类型T的对象的地址
    如:
int a = 112; float c = 3.14;
int* d = &a; float e = &c;

注:定义float类型建议在变量后面加上f。

  • 通过一个指针访问它所指向地址的过程称为间接访问或者引用指针;这个用于执行间接访问的操作符示单目操作符*;取地址符为&

如:

cout<<(*d)<<endl;
cout<<(*e)<<endl;

在这里插入图片描述

总结

关于变量,地址和指针变量小结:

  1. 一个变量有三个重要信息:

    A. 变量的地址位置;
    B. 变量所存的信息;
    C. 变量的类型;

  2. 指针变量时一个专门用来记录变量的地址的变量;通过指针变量可以间接访问另一个变量的值(这个变量有可能是另一个指针变量)

2.左值与右值

数组与指针

int main()
{
	//定义一个数组
	char strHelloWorld[] = {"helloworld"};
	char* pStrHelloWorld = "helloworld";
	pStrHelloWrold = strHelloWorld;	//指针变量的值允许改变
	strHelloWorld = pStrHelloWrold;	//数组变量的值不允许改变
}

StrHelloWorld不可变,strHelloWorld[index]的值可变;
pStrHelloWrold可变,pStrHelloWrold[index]的值可变不可变取决于所指区间的存储区域是否可变;

左值和右值

  • 概念:
    一般说法,编译器为其单独分配了一块存储空间,可以取其地址的,左值可以放在赋值运算符左边;右值指的是数据本身;不能取到其自身地址,右值智能赋值运算右边;
  • 具体分析:
    左值最常见的情况如函数和数据成员的名字;
    右值是没有标识符、不可以取地址的表达式,一般也称之为“临时对象”。
    比如:
 a = b + c;

&a是允许的操作,而&(b+c)不能通过编译,因此a是一个左值,而(b+c)是一个右值。

3.一般指针、数组指针和指针数组

几种C++中的原始指针

  • 一般类型指针T*
    T是一个泛型,泛指任何一种类型;
    如:
int i = 4; int* iP = &i;cout<<(*iP)<<endl;
double d = 4; double* dP = &d;cout<<(*dP)<<endl;
char c = 4; char* cP = &c;cout<<(*cP)<<endl;

注意:不同类型的指针存储空间基本一致,一般都是4字节16进制的整型数。

  • 指针的数组(array of pointers)与数组的指针(a pointer to an array):
    指针的数组T* t[]
    数组的指针 T(*t) []
    如:
int* a[4];
int (*b)[4];	//注意:[]优先级比较高;

举例说明:

int c[4] = {0x80000000,0xFFFFFFFF,0x00000000,0x7FFFFFFF};
int* a[4];	//array of pointers 指针的数组
int(*b)[4];	//a pointer to an array 数组的指针
b = &c;		//注意:这里数组个数得匹配
//将数组c中元素赋给数组a
for(unsigned int i = 0; i < 4; ++i)
{
	a[i] = &(c[i]);
}
//输出看下结果
cout<<*(a[0])<<endl;	//-2147483648
cout<<(*b)[3]<<endl;	//2147483647

4.const与指针

  • const pointer与pointer to const
    例子:
char strHelloworld[] = {"helloworld"};
char const *pStr1 = "helloworld";	//存储空间内容不可变
const char *pStr1 = "helloworld";	//存储空间内容不可变
char* const pStr2 = "helloworld";	//指针不可变
char const * const pStr3 = "helloworld";	//存储空间内容和指针都不可变
pStr1 = strHelloworld;
//pStr2 = strHelloworld;	//pStr2不可改
//pStr3 = strHelloworld;	//pStr3不可改
  • 关于const修饰部分:
    1.看左侧最近的部分;
    2.如果左侧没有,则看右侧;

5.指向指针的指针

  • 指向指针的指针
    例子:
int a = 123;
int* b = &a;
int** c = &b;

在这里插入图片描述

  • *操作符具有从右向左的结合性
    **这个表达式相当于*(*c),必须从里向外逐层求值;
    *c得到的是c指向的位置,即b;
    **c相当于*b,得到变量a的值;
表达式表达式的值
a123
b&a
*ba,123
c&b
*cb,&a
**c*b,a,123

6.关于野指针

  • 未初始化和非法的指针
    例子:
    int *a;int* a;两者没有区别。
int *a;
*a = 12;

注意:a这里指向哪里?这里会发生什么?
运气好的话:定位到一个非法地址,程序会出错,从而终止。
定位到一个可以访问的地址,无意修改了它,这样的错误难以捕捉,引发的错误可能与原先用于操作的代码完全不相干!

用指针进行间接访问之前,一定要非常小心,确保它已经初始化,并被恰当的赋值。

NULL指针

一个特殊的指针变量,表示不指向任何东西(避免了不确定性访问)。
如:

int *a = NULL;

NULL指针的概念非常有用:
它给了一种方法,来表示特定的指针目前未指向任何东西。

使用的注意事项:
对于一个指针,如果已经知道将被初始化为什么地址,那么请赋给它这个地址值,否则请把它设置为NULL
在对一个指针进行间接引用前,请先判断这个指针的值是否为NULL

int main()
{
	//指针的指针
	int a = 123;
	int* b = &a;
	int** c = &b;

	//NULL的使用
	int* pA = NULL;
	pA = &a;
	if(pA != NULL)//判断NULL指针
	{
		cout<<(*pA)<<endl;
	}
	pA = NULL;//pA不用时,置为NULL

	return 0;
}

杜绝"野"指针

指向"垃圾"内存的指针。if等判断对它们不起作用,因为没有置NULL;

  • 一般有三种情况:
    1.指针变量没有初始化;
    2.已经释放不用的指针没有置NULL,如delete和free之后的指针;
    3.指针操作超越了变量的作用范围;

  • 指针使用的注意事项:
    没有初始化的,不用的或者超过范围的指针请把值置为NULL

7.指针的基本操作

&与*操作符

char ch = 'a';char* cP = &ch;

在这里插入图片描述

在这里插入图片描述

int main()
{
	char ch = 'a';
	//&操作符
	&ch = 97;			//&ch左值不合法
	char* cp = &ch;		//&ch右值
	//&cp = 97;			//&cp左值不合法
	char** cpp = &cp;	//&cp右值
	
	//*操作符
	*cp = 'a';			//*cp左值取变量ch位置
	char ch2 = *cp;		//*cp左值取变量ch存储的值
	//*cp + 1 = 'a';	//*cp+1左值不合法的位置
	ch2 = *cp + 1;		//*cp+1右值取到的字符做ASCII码+1操作
	*(cp + 1) = 'a';	//*(cp+1)左值语法上合法,取ch后面位置的值
	ch2 = *(cp + 1);	//*(cp+1)右值语法上合法,取ch后面位置的值
	return 0;
}

++与–操作符

//++,--操作符
char* cp2 = ++cp;

在这里插入图片描述

char* cp3 = cp++;

在这里插入图片描述

char* cp4 = --cp;

在这里插入图片描述

char* cp5 = cp--;

在这里插入图片描述

++操作符进一步说明

在这里插入图片描述
注意:++操作符的优先级高于取间接*操作符的优先级。
在这里插入图片描述

关于++++,----等运算符

编译器程序分解成符号的方法是:一个字符一个字符的读入,如果该字符可能组成一个符号,那么读入下一个字符,一直到读入的字符不再能组成一个有意义的符号。这个处理过程称为"贪心法"。
例如:

int a = 1,b = 2,c;
c = a+++b;	//相当于a++ +b;
d = a++++b;	//相当于a++ ++b,error

在这里插入图片描述

int main()
{
	char ch = 'a';
	char* cp = &ch;
	//++,--操作符
	char* cp2 = ++cp;
	char* cp3 = cp++;
	char* cp4 = --cp;
	char* cp5 = cp--;

	//++ 左值
	//++cp2 = 97;
	//cp2++ = 97;
	
	//*++,++*
	*++cp2 = 98;
	char ch3 =  *++cp2;
	*cp2++ = 98;
	char ch4 = *cp2++;

	//++++,----操作符等
	int a = 1,b = 2,c,d;
	//c = a++b;		//error
	c = a++ + b;
	//d = a++++b;	//error
	char ch5 = ++*++cp;
	
	return 0;
}

8.CPP程序的存储区域划分

栈和队列

在这里插入图片描述
在C++中,一般变量都存储在栈中。

#include <cstddef>
#include <cstring>
using namespace std;

int a = 0;				//(GVAR)全局初始化区
int* p1;				//(bss)全局未初始化区

int main()				//(text)代码区
{
	int b = 1;			//(stack)栈区变量
	char s[] = "abc";	//(stack)栈区变量
	int* p2 = NULL;		//(stack)栈区变量
	const char* p3 = "123456";//123456\0在常量区,p3在(stack)栈区
	static int c = 0;	//(GVAR)全局(静态)初始化区
	p1 = new int(10);	//(heap)堆区变量
	p2 = new int(20);	//(heap)堆区变量
	char* p4 = new char[7];//(heap)堆区变量
	strcpy_s(p4, 7, "123456");	//(text)代码区

	return 0;			//(text)代码区
}

9.CPP程序的存储区域划分总结

代码和数据在C++程序中的存储

在这里插入图片描述
还有一个常量区,在heap和全局GVAR之间。

10.CPP动态分配和回收原则

堆heap

  • 动态分配资源——堆(heap):
  1. 从现代的编程语言角度来看,使用堆,或者说使用动态内存分配,是一件很自然不过的事情。
  2. 动态内存带来了不确定性:内存分配耗时需要多久?失败了怎么办?在实时性要求比较高的场合,如一些嵌入式控制器和电信设备。
  3. 一般而言,当我们在堆上分配内存时,很多语言会使用new这样的关键字,有些语言则是隐式分配。在C++中new的对应词是delete,因为C++是可以让程序员完全接管内存的分配释放的。

分配和回收动态内存的原则

程序通常牵扯到三个内存管理器的操作:

  1. 分配一个某个大小的内存块;
  2. 释放一个之前分配的内存块;
  3. 垃圾收集操作,寻找不再使用的内存块并予以释放;这个回收策略需要实现性能、实时性、额外开销等各方面的平衡,很难有统一和高效的做法;

C++做了1,2两件事;而Java则做了1,3两件事;

11.RAII初步

资源管理方案——RAII

RAII(Resource Acquisition Is Initialization)

  • C++所特有的资源管理方式。有少量其他语言,如D、Ada和Rust也采纳了RAII。但主流的编程语言中,C++是唯一一个依赖RAII来做资源管理的。
  • RAII依托栈和析构函数,来对所有的资源——包括堆内存在内进行管理。对RAII的使用,使得C++不需要类似于Java那样的垃圾收集方法,也能有效地对内存进行管理。RAII的存在,也是垃圾收集。虽然理论上可以在C++使用,但从来没有真正流行过的主要原因。
  • RAII有些比较成熟的智能指针代表:如std::auto_ptr和boost::shared_ptr

12.几种变量的对比

栈和堆中的变量对比

栈(stack)区堆(heap)区
作用域函数体内,语句块{}作用域;整个程序范围内,由new,malloc开始,delete,free结束;
编译间大小确定变量大小范围确定变量大小范围不确定,需要运行期确定;
大小范围Windows系统默认栈大小是1M,linux常见默认的栈大小是8M或10M(通过ulimit -s查看;不同linux发行版的命令不保证相同)所有系统的堆空间上限是接近内存(虚拟内存)的总大小的(一部分被OS占用);
内存分配方式地址由高到低减少地址由低到高增加
内存是否可变可变可变

全局静态存储区和变量存储区的变量对比

全局静态存储区常量存储区
存储内容全局变量,静态变量常量
编译期间大小是否确定确定确定
内容是否可变可变不可变
#include <cstddef>
#include <cstring>
using namespace std;

int a = 0;				//(GVAR)全局初始化区
int* p1;				//(bss)全局未初始化区

int main()				//(text)代码区
{
	int b = 1;			//(stack)栈区变量
	char s[] = "abc";	//(stack)栈区变量
	int* p2 = NULL;		//(stack)栈区变量
	const char* p3 = "123456";//123456\0在常量区,p3在(stack)栈区
	static int c = 0;	//(GVAR)全局(静态)初始化区
	p1 = new int(10);	//(heap)堆区变量
	p2 = new int(20);	//(heap)堆区变量
	char* p4 = new char[7];//(heap)堆区变量
	strcpy_s(p4, 7, "123456");	//(text)代码区

	释放内存///
	//(text)代码区
	if(p1 != NULL)
	{
		delete p1;
		p1 = NULL;
	}
	if(p2 != NULL)
	{
		delete p2;
		p2 = NULL;
	}
	if(p4 != NULL)
	{
		delete[] p4;
		p4 = NULL;
	}
	return 0;			//(text)代码区
}

注意:需要注意的是,不要混用delete(C++)和free ( C)。

13.内存泄漏

内存泄漏(Memory Leak)问题

  • 什么是内存泄漏问题:
    指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
  • 内存泄漏发生原因和排查方式:
  1. 内存泄漏主要发生在堆内存分配方式中,即"配置了内存后,所有指向该内存的指针都遗失了"。若缺乏语言这样的垃圾回收机制,这样的内存片就无法归还系统。
  2. 因为内存泄漏属于程序运行中的问题,无法通过编译识别,所以只能在程序运行过程中来判断和诊断。

14.智能指针auto_ptr

比指针更安全的解决方案

使用指针是非常危险的行为,可能存在空指针。野指针问题,并可能造成内存泄漏问题。
可指针又非常的高效,所以我们希望以更安全的方式来使用指针。

  • 一般有两种典型的方案:
  1. 使用更安全的指针——智能指针;
  2. 不使用指针,使用更安全的方式——引用;

C++的智能指针

  • C++中推出了四种常用的智能指针:
  • unique_ptr、shared_ptr、weak_ptr和C++ 11中已经废弃(deprecated)的auto_ptr,在C++ 17中被正式删除;
  • 这里,我们从应用方面来分析这几种智能指针:
  1. 应用场景:
    A. 对象所有权;
    B. 生命周期;
  2. 性能分析;

auto_ptr

头文件:#include<memory>
在这里插入图片描述
有new_expression获得对象,在auto_ptr对象销毁时,它所管理的对象也会自动被delete掉。

所有权转移:不小心把它传递给另外的智能指针,原来的指针就不再拥有这个对象了。在拷贝/赋值过程中,会直接剥夺指针对原对象对内存的控制权,转交给新对象,然后再将原对象指针置为nullptr。

#include <string>
#include <iostream>
#include <memory>
using namespace std;
int main()
{
	{// 确定auto_ptr失效的范围
		// 对int使用
		auto_ptr<int> pI(new int(10));
		cout << *pI << endl;                // 10 

		// auto_ptr	C++ 17中移除	拥有严格对象所有权语义的智能指针
		// auto_ptr原理:在拷贝 / 赋值过程中,直接剥夺原对象对内存的控制权,转交给新对象,
		// 然后再将原对象指针置为nullptr(早期:NULL)。这种做法也叫管理权转移。
		// 他的缺点不言而喻,当我们再次去访问原对象时,程序就会报错,所以auto_ptr可以说实现的不好,
		// 很多企业在其库内也是要求不准使用auto_ptr。
		auto_ptr<string> languages[5] = {
			auto_ptr<string>(new string("C")),
			auto_ptr<string>(new string("Java")),
			auto_ptr<string>(new string("C++")),
			auto_ptr<string>(new string("Python")),
			auto_ptr<string>(new string("Rust"))
		};
		cout << "There are some computer languages here first time: \n";
		for (int i = 0; i < 5; ++i)
		{
			cout << *languages[i] << endl;
		}
		auto_ptr<string> pC;
		pC = languages[2]; // languges[2] loses ownership. 将所有权从languges[2]转让给pC,
		//此时languges[2]不再引用该字符串从而变成空指针
		cout << "There are some computer languages here second time: \n";
		for (int i = 0; i < 2; ++i)
		{
				cout << *languages[i] << endl;
		}
		cout << "The winner is " << *pC << endl;
		//cout << "There are some computer languages here third time: \n";
		//for (int i = 0; i < 5; ++i)
		//{
		//	cout << *languages[i] << endl;
		//}
	}
	return 0; 
}

注意:使用nullptr代替NULL对指针判断是否为空。

15.智能指针unique_ptr

unique_prt

在这里插入图片描述
unique_prt是专属所有权,所以unique_ptr管理的内存,只能被一个对象持有,不支持复制和复赋值。
移动语义:unique_ptr禁止了拷贝语义,但有时我们也需要能够转移所有权,于是提供了移动语义,即可以使用std::move()进行控制所有权的转移。

#include <memory>
#include <iostream>
using namespace std;
int main()
{
	// 在这个范围之外,unique_ptr被释放
	{
		auto i = unique_ptr<int>(new int(10));
		cout << *i << endl;
	}

	// unique_ptr
	auto w = std::make_unique<int>(10);
	cout << *(w.get()) << endl;                             // 10
	//auto w2 = w; // 编译错误如果想要把 w 复制给 w2, 是不可以的。
	//  因为复制从语义上来说,两个对象将共享同一块内存。

	// unique_ptr 只支持移动语义, 即如下
	auto w2 = std::move(w); // w2 获得内存所有权,w 此时等于 nullptr
	cout << ((w.get() != nullptr) ? (*w.get()) : -1) << endl;       // -1
	cout << ((w2.get() != nullptr) ? (*w2.get()) : -1) << endl;   // 10
    return 0;
}


16.shared_ptr和weak_ptr理论讲解

shared_ptr

在这里插入图片描述
shared_ptr通过一个引用计数共享一个对象。

shared_ptr是为了解决auto_ptr在对象所有权上的局限性,在使用引用计数的机制上提供了可以共享所有权的智能指针,当然这需要额外的开销。

当引用计数为0时,该对象没有被使用,可以进行析构。

  • 循环引用:引用计数会带来循环引用的问题
    在这里插入图片描述
    循环引用会导致堆里的内存无法正常回收,造成内存泄漏。

weak_ptr

在这里插入图片描述
weak_ptr被设计为与shared_ptr共同工作,用一个观察者模式工作。

作用是协助shared_ptr工作,可获得资源的观测权,像旁观者那样观测资源的使用情况。
观察者意味着weak_ptr只对shared_ptr进行引用,而不改变其引用计数,
当被观察的shared_ptr失效后,相应的weak_ptr也相应失效。

17.shared_ptr和weak_ptr代码演示

#include <iostream>
#include <memory>
using namespace std;
int main()
{
	 shared_ptr 
	//{
	//	//shared_ptr 代表的是共享所有权,即多个 shared_ptr 可以共享同一块内存。
	//	auto wA = shared_ptr<int>(new int(20));
	//	{
	//		auto wA2 = wA;
	//		cout << ((wA2.get() != nullptr) ? (*wA2.get()) : -1) << endl;       // 20
	//		cout << ((wA.get() != nullptr) ? (*wA.get()) : -1) << endl;           // 20
	//		cout << wA2.use_count() << endl;                                              // 2
	//		cout << wA.use_count() << endl;                                                // 2
	//	}
	//	//cout << wA2.use_count() << endl;                                               
	//	cout << wA.use_count() << endl;                                                    // 1
	//	cout << ((wA.get() != nullptr) ? (*wA.get()) : -1) << endl;               // 20
	//	//shared_ptr 内部是利用引用计数来实现内存的自动管理,每当复制一个 shared_ptr,
	//	//	引用计数会 + 1。当一个 shared_ptr 离开作用域时,引用计数会 - 1。
	//	//	当引用计数为 0 的时候,则 delete 内存。
	//}

	// move 语法
	auto wAA = std::make_shared<int>(30);
	auto wAA2 = std::move(wAA); // 此时 wAA 等于 nullptr,wAA2.use_count() 等于 1
	cout << ((wAA.get() != nullptr) ? (*wAA.get()) : -1) << endl;          // -1
	cout << ((wAA2.get() != nullptr) ? (*wAA2.get()) : -1) << endl;      // 30
	cout << wAA.use_count() << endl;                                                  // 0
	cout << wAA2.use_count() << endl;                                                // 1
	//将 wAA 对象 move 给 wAA2,意味着 wAA 放弃了对内存的所有权和管理,此时 wAA对象等于 nullptr。
	//而 wAA2 获得了对象所有权,但因为此时 wAA 已不再持有对象,因此 wAA2 的引用计数为 1。

    return 0;
}

#include "stdafx.h"

#include <string>
#include <iostream>
#include <memory>
using namespace std;

struct B;
struct A {
	shared_ptr<B> pb;
	~A()
	{
		cout << "~A()" << endl;
	}
};
struct B {
	shared_ptr<A> pa;
	~B()
	{
		cout << "~B()" << endl;
	}
};

// pa 和 pb 存在着循环引用,根据 shared_ptr 引用计数的原理,pa 和 pb 都无法被正常的释放。
// weak_ptr 是为了解决 shared_ptr 双向引用的问题。
struct BW;
struct AW
{
	shared_ptr<BW> pb;
	~AW()
	{
		cout << "~AW()" << endl;
	}
};
struct BW
{
	weak_ptr<AW> pa;
	~BW()
	{
		cout << "~BW()" << endl;
	}
};

void Test()
{
	cout << "Test shared_ptr and shared_ptr:  " << endl;
	shared_ptr<A> tA(new A());                                               // 1
	shared_ptr<B> tB(new B());                                                // 1
	cout << tA.use_count() << endl;
	cout << tB.use_count() << endl;
	tA->pb = tB;
	tB->pa = tA;
	cout << tA.use_count() << endl;                                        // 2
	cout << tB.use_count() << endl;                                        // 2
}
void Test2()
{
	cout << "Test weak_ptr and shared_ptr:  " << endl;
	shared_ptr<AW> tA(new AW());
	shared_ptr<BW> tB(new BW());
	cout << tA.use_count() << endl;                                        // 1
	cout << tB.use_count() << endl;                                        // 1
	tA->pb = tB;
	tB->pa = tA;
	cout << tA.use_count() << endl;                                        // 1
	cout << tB.use_count() << endl;                                        // 2
}

int main()
{
	Test();
	Test2();

    return 0;
}


18.引用

C++的引用

  • 引用是什么?
    是一种特殊的指针,不允许修改的指针。

使用指针有哪些坑:

  1. 空指针
  2. 野指针
  3. 不知不觉改变了指针的值,却继续使用;

使用引用,则可以:
4. 不存在空引用;
5. 必须初始化;
6. 一个引用永远指向它初始化的那个对象;

  • 引用的基本使用:可以认为是指定变量的别名,使用时可以认为是变量本身;
int x = 1,x2 = 3;
int& rx = x;
rx = 2;
cout<<x<<endl;	//2
cout<<rx<<endl;	//2
rx = x2;
cout<<x<<endl;	//3
cout<<rx<<endl; //3
  • 有了指针为什么还需要引用?
    Bjarne Stroustrup(C++之父)的解释:
    为了支持函数运算符重载
  • 有了引用为什么还需要指针?
    Bjarne Stroustrup(C++之父)的解释:
    为了兼容C语言;
    注意:C语言完全用指针,JAVA语言完全用引用。

补充

关于函数传递参数类型的说明:

  1. 对内置基础类型(如int,double等)而言,在函数中传递pass by value更高效;
  2. 对OO面向对象中自定义类型而言,在函数中传递时pass by reference to const更高效
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值