C++笔记之指针与数组

1.指针与数组的基本概念

1.1数组概念

数组(Array)是有序的元素序列。元素的类型相同,元素的个数确定。(如果元素个数不确定建议使用vector),数组元素不仅在逻辑上是一个接一个连续的,在实际存储中也是,数组中的元素存储在一个连续性的内存块中,并通过索引(下标)来访问。

1.2指针概念

指针是一种指向某种类型的复合类型,用于记录地址。指针也是对象,可以赋值和拷贝。指针占内存空间为8字节,这与操作系统的位数有关(见《C++笔记一》第七节指针)。指针存放的是地址,64位系统显然可以有64个“位置”给0、1来排列组合以表示地址,所以地址长度就是64比特也就是8字节。内存最大也就是2的64次方比特。输出指针输出的是变量的起始地址,是其所占内存的第一个存储单元地址(地址最小的一个)

2.指针与数组的联系

数组的每一个元素都是对象,所以可以通过取地址符,使得指针指向任何一个元素。同时在大多数情况下,数组名会被编译器自动转换成指向首地址的指针。而且指针也是迭代器。

2.1指针与数组元素

由于数组的每一个元素都是对象,所以可以对每一个元素进行取地址,然后赋值给指针。

int a[3] = {
    
     1,2,3 };
int *p = &a[2];

2.2数组名与指针的关系

不仅如此,大多数情况下编译器会将数组名换成指向数组首元素的指针进行操作。

2.2.1首先看看反例,数组名在某些情况下不会转换成指针

int arr[5] = {
    
    1,2,3,4,5};
auto p1(arr);
cout<<"arr大小:"<<sizeof(arr)<<endl;  //20
cout<<"p1大小:"<<sizeof(p1)<<endl;    //8
decltype(arr) p2;
cout<<sizeof(p2)<<endl; //返回20,可见这里arr就是数组,而且大小都固定了

由上例可知,至少在使用sizeof()函数与decltype()函数时,数组名没有直接转变成指针。要不然,sizeof返回的就是8,decltype()获取的就是数组类型而不是指针类型

2.2.2如下几种情况数组会转换成指针
1>由于数组不能拷贝,所以当数组被传给函数时,数组会转换成指针。(参考《C++primer》P193、《essential C++》P69)
既然数组会被当作指针传入,那么下面三种函数效果完全一样。
函数声明:

void my_function (int *a);
void my_function (int a[]);
void my_function (int a[200]);

函数调用:

int b;
int *a = &b;
int ar[2] = {
    
    1,2};
my_function(a); //正确
my_function(ar);//正确

数组作为函数输入既然会被当作指针,那么函数的形参与实参写成指针形式与数组形式都可以,传入的是数组的话,只要不越界就可以。但有一个问题,数组当指针传入时只记录了数组首地址,无法记录数组大小。解决方案主要有二。其一,增加一个参数,表示数组大小。其二,增加一个“哨兵”尾指针(数组尾元素的后一位,通过&array[n]获得),在STL中基本上用的都是第二种,其实指针相对数组来说好比迭代器相对容器,也就是说指针就是一个迭代器。这本文后面会有详细的解释。

数组作为形参时,如果不想改变数组的话可以加上const。而且也可以加上引用,只不过限制会多一些。比如:

void func(int (&a)[10]);

那么此时只能是int类型且长度为10数组才能作为实参。

2>由于数组不能拷贝,从函数中传出时,也会转换成指针。(参考《C++primer》P205)
示例:

int *test()
{
    
    
	int *a = new int[10];
	for (int i = 0; i < 10; i++)
	{
    
    
		*(a+i) = i;
	}
	return a;
}

int main()
{
    
    
	int *b = test();
	for (int i = 0; i < 10; i++)
	{
    
    
		cout << *(b+i) << "\t";
	}
	cout << endl;
	delete[]b;
	return 0;
}

虽然数组作为函数返回值时会变成数组首地址,但是上面代码有些缺陷,不够直观的展示返回值是数组,而且数组大小不够明确。那么,如何解决?可以返回指向数组的指针。
示例:

typedef int arr1[10];	//简化
using arr2 = int[10];	//简化

arr1* func1()
{
    
    
	static int a[10] = {
    
     1,2,3,4,5,6,7,8,9,10 };
	return &a;
}
//不简化
int(*func2())[10]
{
    
    
	static int a[10] = {
    
     1,2,3,4,5,6,7,8,9,10 };
	return &a;
}

void test()
{
    
    
	arr2 *b = func1();	//b是指向数组的指针,解引用后依然是指针
	cout << *(*b + 2) << endl;	//输出3
	
	int a[10];
	decltype(a) *c = func2();	//decltype前面有提到过见2.3.1
	cout << *(*c + 5) << endl;	//输出6
}

2.3指针也是迭代器

2.3.1注意事项

以下的一些操作仅仅适合指针与数组,如果指针指向某些容器则不一定适合!!!**
示例:

//假设有这么一个指针,指向vector<int>
vector<int> *pseq;
//我们像读取vector中第二个元素
int elem = (*pseq)[1];

pseq指向容器,所以先解引用获取容器,在用下标(vector对象可以通过下标索引,因为重载了[ ]操作符),但是如果对数组而言呢?根本不需要解引用,指针指向数组,指针可以直接通过下标操作!

2.3.2指针也是迭代器
首先看个示例:

int arr[3] = {
    
     1,2,3 };
int *p = arr;
cout << *(p + 2) << endl;	//输出3

这里第二行代码与 vector< int >::iterator it = v.begin(); 像不像?把p看出迭代器,arr是容器,由于数组名大多数情况下会被编译器转换成指向首地址的指针,所以这里arr不就是begin()的效果吗?实际上指针就是迭代器。而且vector的迭代器与string的迭代器支持的所有操作,数组的指针全部支持!!!

2.3.3指针作为迭代器时的一些用法
1>指针作为迭代器时,库函数中的begin与end
既然指针作为迭代器,那么对于数组,C++提供了begin()、end()两个函数,只不过数组不是类类型,无法作为类成员函数使用,所以,数组只能作为begin与end的参数传入,头文件为:#include< iterator >。begin()返回指向数组首地址的指针,end()返回指向数组尾元素下一位置的指针。
示例:

void test()
{
    
    
	int a[10] = {
    
     9,8,7,6,5,4,3,2,1,0 };
	int *pb = begin(a);
	int *pe = end(a);
	for (int *p = pb; p != pe; p++)
	{
    
    
		cout << *p << "\t";
	}
}

2>指针运算(指针加某整数值、指针相减)
指针加某个整数值,得到仍然是指针,但是需要注意的是即使得到了新指针也需要在begin()与end()之间(指向数组元素或者数组尾元素的下一位)。对指针进行加减操作则是以sizeof(Type)为移动单位(包括指向自定义数据类型的指针),并不是简单的地址加1(移到下一个存储单元)。
补充:有关存储单元的一些内容
存储单元是CPU访问存储器的基本单位。以8位二进制作为一个存储单元,也就是一个字节。也就说存储单元的大小是恒定不变的就是一个字节。
示例:

void test()
{
    
    
	int a[10] = {
    
     9,8,7,6,5,4,3,2,1,0 };
	int *p = a;
	cout << *(p + 5) << endl;	//4
}

指针相减:指针相减得到的是两元素之间的距离,比如end-begin得到数组元素个数。
示例:

void test()
{
    
    
	int a[10] = {
    
     9,8,7,6,5,4,3,2,1,0 };
	int *pb = begin(a);
	int *pe = end(a);
	cout << pe - pb << endl;	//10
}

3>解引用与指针运算的交互,指针通过运算跳转到数组其他元素的地址时,可以通过解引用获取元素其值。其实前面指针运算的示例已经用到了解引用。
示例:

void test()
{
    
    
	int a[10] = {
    
     9,8,7,6,5,4,3,2,1,0 };
	int *p = a;
	cout << *(p + 5) << endl;	//4
}

4>指针与下标,指针也可以通过下标进行索引。
示例:

void test()
{
    
    
	int a[10] = {
    
     1,2,3,4,5,6,7,8,9,10 };
	int *p = a;
	cout << p[5] << endl;	//6
}

3.指针使用时的几个隐患以及建议

指针使用时几个隐患以及建议(参考《C++primer》P47指针、P105指针与数组、《C++语言程序设计》P243指针的安全性隐患及其应对方案)

3.1 地址安全性问题

3.1.1常见隐患:
其一:拷贝或者访问野指针(野指针:所有地址无效的指针,包括未初始化的)
1> 未初始化就使用
2> free之后未置空的指针
3> 返回局部变量的指针或者引用
其二:访问空指针
其三:指针的算术运算,通过算术运算发生了数组越界,便会有问题

3.1.2解决方法:
1> 初始化所有指针(虽然指针不必创建就初始化),如果所指对象不确定先置空。避免出现野指针
2> free后指针置空,避免出现野指针
3> 避免出现返回局部变量指针与引用的情况,真出现根据实际情况换成静态变量或者在堆中new一个
4> 指针使用前先判断是否为空,而且最好看看是否指向合法空间
5> 尾指针无法解引用与递增操作(数组与指针)
6> 使用vector更加安全,即使越界了也能很快发现,如果要用数组和指针,那就多关注下标!

3.2类型安全性

3.2.1产生原因:
其一:指针类型转换本身带来的隐患。基本数据类型与类类型在进行类型转换时是基于内容的转换。比如整型转换成浮点型,它会将整型的二进制转换成浮点型的二进制。但是指针却不一样,指针的类型只是对数据的解释,指针只存储数据的地址,所以对指针进行类型转换可能会出问题。比如一个int 类型的指针,数据是整型,通过类型转换后,变成char类型的指针,但是数据依然是整型(因为只是把数据解释为char类型而已),而此时却是char类型指针,对其数据的操作显然要符合char类型的操作,也就是说对一个整型进行字符型的操作。
简单来说:基本数据类型与类类型是基于内容的类型转换,但指针却只是更改了对数据的解释,就可能造成使用reinterpret_cast强转后,对int类型数据进行char类型的操作。

其二:void * 带来的隐患 。虽然C++不允许void *隐式转换成其他类型(可强制转换,因为void * 本身就是所指对象确定,但是类型不确定,真的要使用,肯定要先转换成某种具体类型,只允许强转会相对安全些),而且任何类型都可以隐式转换成void *。问题来了,要注意了,比如我们使用malloc函数,开辟了一些空间,我们打算是int类型的,malloc返回的结果是一个void *,这里就相当于其他类型隐式转换成void *,然而我们要使用必然要将void *显示转换成其他具体类型,如果我们转换成int类型,即与转换成void *之前的指针类型一致,那么这是安全的!!!如果不一致,那又会出现reinterpret_cast同样的隐患。(malloc的使用是最好例子)
简单来说:其他类型指针转换成void * 后,再使用void * ,需要强制转换,而且最好与最开始的指针类型一致,否则又会出现reinterpret_cast同样的隐患。

3.2.2常见隐患:
其一:reinterpret_cast本身不安全性,reinterpret_cast会强制转换指针的类型,所以有可能出现上述问题。但同样是类型转换,static_cast就会相对安全些,char型指针转int型指针static_cast是不允许的,然而有void *存在,所以static_cast也不一定安全,见第二种隐患

其二:void * 带来的隐患 。虽然static_cast可以将void * 强转为其他类型的指针,但是如果与最开始的指针类型不匹配,显然又出现了第一种隐患。

3.2.3解决方法:
其一:除非迫不得已,除非非常特殊的底层用途,reinterpret_cast不要使用。
其二:凡是从C语言继承了void * 的函数最好不用,(void * 的两个作用见《C++笔记一C语言基础》7.4节。两种作用在C++中基本都有替代的)
其三:非要使用void * 那么在将void * 强转为其他类型时一定要转换成最初的指针类型。

3.3堆对象的管理

3.3.1产生原因:
一个堆对象的指针被传递给多个对象,那么就会有在什么时候应该由哪个对象,哪个函数负责删除对象的问题。比如一个函数需要返回一个对象,但是为了避免拷贝,在堆中new了该对象,返回该对象的指针。某个函数调用了该函数,那么就应该由该函数三删除对象。这种情况要秉承着谁调用谁销毁的原则,在new对象的函数中注释谁调用随销毁。总之:一定要理清每一个堆对象的归属问题!

3.3.2常见隐患:
堆对象可能被多个对象接收,如果释放的不好,可能造成与返回局部变量指针或引用相同问题,使用一个被销毁的地址是会出问题的!!!

3.3.3解决方法:
其一:借助共享指针
其二:理清每一个堆对象的归属就好解决

猜你喜欢

转载自blog.csdn.net/weixin_45884870/article/details/112426469