【零基础C++从入门到精通】(五) 数组与指针


本章将要讲解的数组与指针是两个非常重要的概念,两者从C语言的时代就已经存在了。数组和指针都可以像标准库的vector一样顺序地遍历一组数据,然而它们与vector相比却能更直接地接触到计算机的内存,因此在高效的同时也会有更大的风险。在有了标准库以后,我们可以直接用vector替代数组,所以在一般情况下我们建议只使用vector。

5.1 数组

在上一章中我们讲解了vector,vector的push_back()操作可以无限地添加元素,因此vector是动态的。不过所谓的动态也不是说每放一个元素进去就会多占用一点内存,关于vector的具体实现我们在标准库的章节会讲到。与vector不同,数组是一种完全静态的数据结构,在初始化的时候我们就需要给数组指定大小,并且不能修改。

5.1.1 数组的创建及初始化

vector在创建的时候需要元素类型和名称两个信息,而vector的大小可以在初始化的时候指定,也可以不指定。由于数组是静态的,因此我们一定要为其指定大小,这也称作数组的维度( Dimension )。数组的维度必须像switch的case后面的表达式那样,是一个在编译的时候就能确定的整型常量表达式。下面我们来看几个数组创建的示例。

#include <iostream>
using namespace std;

// 数组的创建

int main() {
	int arr1[2];
	const int constNum = 4;
	float arr2[constNum];
	int arr3[constNum + 1];
	int num = 5;
	char arr4[num];
	return 0;
}

上面展示了数组的创建。arrl直接使用整型字面量作为数组大小,这没有什么问题;arr2使用const int,在编译的时候值也是确定的; arr3使用了const int和字面量的算术表达式,值也可以确定;最后的arr4由于使用了变量,虽然在num初始化和数组初始化之间并没有不确定的改变,但是使用变量就是不合法的,编译程序会报出如图所示的错误:
在这里插入图片描述

初始化列表

#include <iostream>
using namespace std;

// 数组的初始化列表

int main() {
	int arr1[5] = { 0, 1, 2, 3, 4 };
	int arr2[5] = { 0, 1, 2 };
	int arr3[] = { 0, 1, 2, 3, 4 };
	return 0;
}

展示了初始化列表的几种用法。我们可以看到,在花括号中的数字就是我们按顺序给数组每个元素赋的值。
arrl的初始化列表中的值个数与数组大小相等,
而arr2中的值个数比数组大小少,在这种情况下没有初始化的元素依然有着默认的值。
如果初始化列表中元素个数超出了数组大小,那么编译器会报错。
要注意,在最后的arr3中我们没有给定数组的大小,在这种情况下数组大小会随着初始化列表的大小而确定。

5.1.2 数组的操作

数组的下标操作与vector的下标操作类似,也会有越界的问题。

#include <iostream>
using namespace std;

// 数组的操作

int main() {
	int arr1[5] = { 0, 1, 2, 3, 4 };
	int arr2[5] = { 0, 1, 2 };
	int arr3[] = { 0, 1, 2, 3, 4 };
	for (int i = 0; i < 5; i++) {
		cout << arr1[i] << " ";
	}
	cout << endl;
	for (int i = 0; i < 5; i++) {
		cout << arr2[i] << " ";
	}
	cout << endl;
	for (int i = 0; i < 5; i++) {
		cout << arr3[i] << " ";
	}
	cout << endl;
	return 0;
}

在这里插入图片描述
数组的下标操作也可以用**“arr2[i]=arrl[i]”**的形式将元素赋值给另一个数组,以达到复制的效果。使用下标操作符赋值单个数组元素也是因为数组不能像vector那样直接用arrl和arr2这样的数组名将整个数组的内容赋值到另一个数组。vector可以使用“vec1=vec2”这样的形式赋值,而“arr2=arr1”就会产生编译错误。

5.2 指针

指针是C++中的一个核心概念,是一名C++程序员可以直接对内存进行操作的一种工具。这样的工具就是一把双刃剑,一方面可以实现一些非常优化的程序,另一方面也会导致一些难以调试的错误。

5.2.1 使用指针遍历数组

#include <iostream>
using namespace std;

// 使用指针遍历数组

int main() {
	int arr[5] = { 0, 1, 2, 3, 4 };
	int* ptr = arr;
	for (int i = 0; i < 5; i++) {
		cout << *ptr << " ";
		ptr++; // 也可以直接写成 cout << *(ptr++) << " ";
	}
	cout << endl;
	return 0;
}

在这里插入图片描述

5.2.2 指针的概念与理解

指针(Pointer ),从其英文字面上来理解就是一个指向某一物件的东西,在程序中就是指向数据的地址(Address)。计算机的内存可以看作是一个紧密排列的数据序列,每一小块数据(也就是字节)的旁边都有一个编号代表数据的地址。这在现实中可以用房屋的地址来理解,我们可以说一栋房子是小张的家(变量名表示),也可以说一栋房子是XX路XXX号(指针表示)。对于上一小节的示例,我们可以根据图6.2.2来理解ptr和arr到底指的是什么。

在这里插入图片描述
假设arr的地址是203,那么数组后面几个元素的地址依次递增(这个例子中因为数组的类型是int,所以其实真实的地址需要依次加4字节)。之前我们说过,指针实际上就是内存地址,所以arr的值就是203,而当ptr指向数组最后一个元素的时候,它的值是207。如果我们想要获取某一个地址下存储的数据,就可以使用*ptr来获得。

#include <iostream>
using namespace std;

// 指针的含义

int main() {
	int arr[5] = { 0, 1, 2, 3, 4 };
	int* ptr = arr;
	for (int i = 0; i < 5; i++) {
		cout << *ptr << " ";
		cout << "地址:" << ptr << endl;
		ptr++;
	}
	return 0;
}

在这里插入图片描述
每个元素之间的间距正好是int的大小——4字节。

5.2.3 指针的创建与初始化

#include <iostream>
using namespace std;

// 指针的创建和初始化

int main() {
	float* floatPtr = NULL;
	string* strPtr;
	int* intPtr1, * intPtr2;
	int* intPtr3, intPtr4; // intPtr4只是一个整数
	return 0;
}

正确的语法应该像intPtr2那样在前面加一个星号,不管与星号之间有没有空格。

除了上面例子中的那些指针类型外,C++还有一种通用的void指针。我们知道指针就是地址,指针的类型只不过表示了地址指向的位置所存放的数据类型。如果我们将int指针转换为float指针,那么程序也只是将数据重新解读为浮点类型。所以这里void只是代表了一个地址,而我们不知道它所指向的数据类型,但我们也可以重新定义它所指向的数据类型。void*一般会在一些内存处理的系统函数中使用

5.2.4 指针的基本操作

对于指针来说,解引用和取地址是最重要的两个操作符。

#include <iostream>
using namespace std;

// 指针的基本操作

int main() {
	int num = 4;
	int* intPtr = &num;
	cout << "num的地址是:" << &num << endl;
	cout << "指针的值是:" << intPtr << endl;
	if (intPtr) { // 检查指针是否为空
		cout << "指针所指的数字是:" << *intPtr << endl;
	}
	return 0;
}

在这里插入图片描述
解引用操作符“*”
上面代码用解引用操作符读取了指针指向的数据,而解引用操作符也可以用来作为赋值语句的左值以修改数据。

#include <iostream>
using namespace std;

// 左值解引用

int main() {
	int num = 4;
	int* intPtr = &num;
	if (intPtr) { // 检查指针是否为空
		cout << "指针所指的数字是:" << *intPtr << endl;
		cout << "num的值是:" << num << endl;
		*intPtr = 3;
		cout << "修改后,指针所指的数字是:" << *intPtr << endl;
		cout << "num的值是:" << num << endl;
	}
	return 0;
}

在这里插入图片描述
在这里插入图片描述
图6.2.6中的变量num由两部分组成,上半部分“intPtr”是地址,而下半部分“*intPtr”是地址中实际存储的数据。intPtr的值是地址100,*intPtr会返回数据4。右上角的&num取得变量num的地址100,可以赋值给类似intPtr的指针变量。图片下方展示了左值解引用的行为,*intPtr在这里取得修改num的权限,然后3将会被赋值到num所在的地址,覆盖掉原有的值4。

5.2.5 指针的算术操作

指针可以像整型那样进行一部分算术操作,还可以对地址进行修改。因为计算后的指针不一定会指向具有有效数据的地址,所以我们在进行指针算术操作的时候需要格外小心。

#include <iostream>
using namespace std;

// 指针与整型的算术操作

int main() {
	int arr[5] = { 0, 1, 2, 3, 4 };
	int* ptr = arr;
	cout << "arr + 4:" << *(arr + 4) << endl;
	cout << "ptr + 4:" << *(ptr + 4) << endl;
	cout << "ptr:" << ptr << endl;
	cout << "ptr + 2:" << ptr + 2 << endl;
	cout << "++ptr:" << ++ptr << endl;
	cout << "ptr - 2:" << ptr - 2 << endl;
	cout << "--ptr:" << --ptr << endl;
	return 0;
}

在这里插入图片描述
我们可以看到,指针与整型的算术操作不同于一般的数字加减,而是与指针的类型绑定的。由于一个int的大小是4字节,那么ptr+2会将地址加上8,在数组中就是指向第三个元素。在示例中,除了指针ptr,我们也对数组名arr做了加法,得到的结果都是第五个元素的值。此外,示例末尾的0042FE88已经比数组第一个元素的地址还小了,如果对这个地址解引用,可能会导致程序崩溃。
指针除了与整型的算术操作之外,还可以进行指针相减

#include <iostream>
using namespace std;

// 指针相减

int main() {
	int arr[5] = { 0, 1, 2, 3, 4 };
	int* ptr1 = arr + 1;
	int* ptr2 = arr + 3;
	cout << "ptr1:" << ptr1 << endl;
	cout << "ptr2:" << ptr2 << endl;
	cout << "ptr2 - ptr1:" << ptr2 - ptr1 << endl;
	cout << "ptr1 - ptr2:" << ptr1 - ptr2 << endl;
	return 0;
}

在这里插入图片描述
指针相减返回的是指针地址之间的距离,并且是分正负的。这个距离也与类型绑定,单位是该类型数据的个数。指针之间不存在加法,每个指针代表的地址在计算机中都是唯一确定的,相加没有任何意义。

5.2.6 const 指针

我们之前讲解了使用左值解引用来修改指针指向的原变量的例子,但如果原变量是const,值是不能被修改的,因此我们也需要有一种特殊的指针来保证原变量不会被修改,这就是指向const对象的指针。

#include <iostream>
using namespace std;

// 指向const对象的指针

int main() {
	const int num = 3;
	// 普通指针不能指向const变量
	// int *ptr1 = &num; 
	const int* ptr2 = &num;
	cout << "*ptr2: " << *ptr2 << endl;
	// 指向const对象的指针不能修改解引用后的值
	// *ptr2 = 4;
	// 指向const对象的指针可以修改指向的地址
	const int num1 = 4;
	ptr2 = &num1;
	cout << "*ptr2: " << *ptr2 << endl;
	// 指向const对象的指针也可以指向普通变量
	int num2 = 5;
	ptr2 = &num2;
	cout << "*ptr2: " << *ptr2 << endl;
	return 0;
}

在这里插入图片描述
我们可以看到,要定义一个指向const对象的指针,我们就要在const对象类型名后加上星号。**"int *ptrl = #”**这一行如果去掉注释,编译器就会报错,因为普通指针不能指向const对象。“*ptr2 =4;”这一行如果去掉注释相当于修改const对象的值,编译器也会报错。
这里需要注意的是,虽然ptr2指向的地址中的值不能修改,但是它本身指向的地址却可以修改。在示例中,我们先后又让它指向了另外两个变量,其中也有一个非const的变量,指向非const变量的这一种指针也不能修改解引用后的值。
既然指向const对象的指针还是可以修改地址的,那么应该也有另外一种不能修改地址的指针,也就是const指针。

5.2.7 指针的数组和数组的指针

指针作为一种变量类型,自然可以被声明为数组,而数组作为一种变量类型,也可以有指向它的指针。所以指针的数组是一种数组,而数组的指针则是一种指针

#include <iostream>
using namespace std;

// 指针的数组和数组的指针

int main() {
	int arr[5] = { 0, 1, 2, 3, 4 };
	// 数组的指针
	int (*arrPtr)[5] = &arr; 
	// 指针的数组
	int *ptrArr[5] = { &arr[0], &arr[1], &arr[2], &arr[3], &arr[4] };
	cout << "arrPtr: " << arrPtr << endl;
	cout << "*arrPtr: " << *arrPtr << endl;
	for ( int i = 0; i < 5; i++ ) {
		cout << ( *arrPtr )[i] << " ";
		cout << ptrArr[i] << " ";
		cout << *( ptrArr[i] ) << endl;
	}
	return 0;
}

在这里插入图片描述
我们可以看到,数组的指针和指针的数组的语法区别在于:数组的指针需要在星号和变量名外面加一个括号,而指针的数组却没有。这一点其实很好理解,因为声明数组的时候元素类型名int和数组大小[5]就是被变量名隔开的,在这里我们添加一个星号,并用括号括起来,表示这个**指针int (*arrPtr)[5]*是指向整个数组的;如果不加括号,编译器就只会将星号联系到前面的类型名int,所以ptrArr就只是声明了一个数组,数组的元素类型是int

在声明arrPtr的时候,我们把数组的地址赋值给它作为初值,由于数组的指针解引用以后就相当于数组,我们可以用**(*arrPtr)i]*来读取数组的元素。
ptrArr是一个指针的数组,它的每一个元素都是一个指针,在这里我们就将数组每个元素的地址分别赋值,而在遍历的时候我们使用
( ptrArr[i])来读取数组中某一个指针指向的元素值。

5.2.8 指针的指针

指针可以指向任何变量或者对象,所以也可以指向指针。

#include <iostream>
using namespace std;

// 指针的指针

int main() {
	int num = 3;
	int* numPtr = &num;
	int** numPtrPtr = &numPtr;
	cout << "num: " << num << endl;
	cout << "*numPtr: " << *numPtr << endl;
	cout << "numPtr: " << numPtr << endl;
	cout << "*numPtrPtr: " << *numPtrPtr << endl;
	cout << "numPtrPtr: " << numPtrPtr << endl;
	return 0;
}

在这里插入图片描述

5.2.9 const_cast与reinterpret_cast

之前讲过C++有几个自己的类型转换操作符,现在在讲解了指针以后,我们就可以比较好地讲解const_cast和reinterpret_cast了。
const_cast的作用是将一个变量转换成const限定的常量。

#include <iostream>
using namespace std;

// const_cast

int main() {
	int intNum = 2;
	intNum = 3;
	// const_cast后面必须跟引用或指针类型
	const int& constIntNum = const_cast<int&>(intNum);
	// 转换以后数字不能再修改
	// constIntNum = 2;
	return 0;
}

展示了const_cast的用法,我们看到intNum在转换前是可以修改的变量,在转换以后就变成常量,不能再进行修改了。
reinterpret_cast比较特殊。reinterpret的意思是重新解读,而reinterpret_cast就是将一段数据按照二进制表示重新解读成另—种数据,所以它其实并没有对数据做任何改变,只是改变了类型。

#include <iostream>
using namespace std;

// reinterpret_cast

int main() {
	int intNum = 0x00646362;
	int* intPtr = &intNum;
	char* str = reinterpret_cast<char*>(intPtr);
	cout << "str的值为:" << str << endl;
	return 0;
}

在这里插入图片描述
reinterpret_cast将一个指向整数的指针转换成了指向字符的指针,也就是C风格的字符串。十六进制的62、63和64在ASCII码中分别代表b、c和d,所以最后打印出了"bcd”。

5.3 动态数组

在本章第一节中我们讲解了数组的相关知识,那一种数组又称为静态数组,因为它的大小从头到尾都是固定的。在本小节中我们将会讲解动态数组的相关知识。动态数组,严格意义上来说,并不是数组,程序会使用指针来承载malloc()或者new操作符动态分配的内存空间,然后在需要更新数组大小或者释放空间的时候使用free()或者delete

5.3.1 使用malloc()和free()动态分配内存

malloc()函数可以在一个叫作堆(Heap)的内存空间中分配指定字节数的内存。与作用域中在栈(Stack)中分配内存的局部变量不同,堆中的内存一旦分配,就不会自动被释放,直到程序调用free()函数

#include <iostream>
using namespace std;

// 使用malloc

int main() {
	int* arr = (int*)malloc(5 * sizeof(int));
	int* ptr = arr;
	for (int i = 0; i < 5; i++) {
		*ptr = i;
		cout << *ptr << " ";
		ptr++; // 也可以直接写成 cout << *(ptr++) << " ";
	}
	cout << endl;
	free(arr);
	arr = NULL;
	return 0;
}

在这里插入图片描述
先使用malloc()分配了大小为“5* sizeof(int)”字节的内存。malloc()的分配单位是字节,所以我们要用sizeof操作符获取整型int的字节数。
此外,由于malloc()返回void指针,因此我们也要将返回值转换为需要的指针类型;分配完内存之后我们就用另一个指针来遍历这个动态的数组,最后需要记得调用free()函数释放内存。
另一个值得注意的点是,在调用free()函数的时候我们不能使用ptr,而必须使用arr。这是因为ptr在遍历结束的时候指向动态数组的末尾元素,如果使用free()释放5
sizeof(int)字节内存,就会触碰到最后一个元素后面未知的内存段。

5.4 多维数组

数组可以表示线性一维的数据序列,而现实世界中存在着许多二维、三维,甚至高维的数据序列。为了表示这些多维的数据序列,我们就需要使用多维数组。

5.4.1 多维数组创建与初始化

#include <iostream>
using namespace std;

// 多维数组的创建

int main() {
	int arr1[3][3] = { {0, 1, 2},
					   {3, 4, 5},
					   {6, 7, 8} };
	int arr2[3][3] = { 0, 1, 2, 3, 4, 5, 6, 7, 8 };
	int arr3[2][2][2] = { { {0, 1},
							{2, 3} },
						{ {4, 5},
						  {6, 7} } };
	return 0;
}

5.4.2 多维数组的遍历

需要使用多重循环

#include <iostream>
using namespace std;

// 多维数组的遍历

int main() {
	int arr1[3][3] = { {0, 1, 2},
					   {3, 4, 5},
					   {6, 7, 8} };
	for (int i = 0; i < 3; i++) {
		for (int j = 0; j < 3; j++) {
			cout << arr1[i][j] << " ";
		}
		cout << endl;
	}
	return 0;
}

在这里插入图片描述

5.4.3 多维数组与数组

#include <iostream>
using namespace std;

// 多维数组的理解

int main() {
	int arr1[3][3] = { {0, 1, 2},
					   {3, 4, 5},
					   {6, 7, 8} };
	cout << "arr1: " << arr1 << endl;
	for (int i = 0; i < 3; i++) {
		cout << "arr1[" << i << "]: " << arr1[i] << endl;
		for (int j = 0; j < 3; j++) {
			cout << arr1[i][j] << " ";
		}
		cout << endl;
	}
	return 0;
}

在这里插入图片描述

5.5 引用

引用(Reference)是C++在C指针的基础上更新改进的一个概念。引用在使用的时候比指针方便,并且不需要考虑类似空指针的问题。不过我们并不能将它等同为指针,这两者在编程中的分工还是比较明确的。

5.5.1 引用的使用

引用的本质是一个变量的别名( Alias ),因此它一定要与某个变量绑定。

#include <iostream>
using namespace std;

// 引用的使用

int main() {
	int num = 3;
	int& numRef = num;
	cout << "num是" << num << ",numRef是" << numRef << endl;
	numRef = 4;
	cout << "num是" << num << ",numRef是" << numRef << endl;
	return 0;
}

在这里插入图片描述
我们可以看到,引用和指针在语法上的区别就是在创建的时候把“*”改成“&”。这里我们把numRef与num绑定,也就是说numRef就是num的别名,因此numRef的值与num相同;而当我们修改numRef的时候,我们实际也在修改num,这在运行结果中可以体现。如果我们不给numRef初始化,编译器将会报错。

5.5.2 引用与指针的区别

引用与指针有如下两点区别:
1.引用必须初始化为某个变量的别名,而指针却可以为空。
2.修改引用时修改的是引用所代表的原变量的值,而修改指针时则是修改指针所指向的地址

总结

这一章介绍了C++中的几个重要概念:数组、指针和引用。数组是一串数据序列,数组名代表着首元素的地址,但是不能被修改。数组的类型和大小在创建时就是固定的。指针是存放内存地址的变量,可以用解引用操作获取地址中存放的数据。引用是变量的别名,必须从一开始就与变量绑定,不能凭空存在。在讲解了这些概念的基本语法和应用后,我们也介绍了一些特殊的情况,比如指针的指针、多维数组以及动态内存的分配。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大桃子技术

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值