跳转到内容

数组、指针和引用

数组是一组相同类型数据的集合,指针是可以操作内存数据的变量,引用是变量的别名。数组的首地址可以看做是指针,通过指针可以操作数组,指针和引用在函数参数传递中可以相互替代。指针是一把双刃剑,使用得好能够带来效率的提升,使用不当也会给程序带来意想不到的灾难。

1. 一维数组

1.1 一维数组声明

程序设计中,将一组数据类型相同的数据按一定形式有序组织起来,这样的线性序列称为数组。每个数组都有一个数组名,通过数组名和下标可以唯一确定一个数组元素。

cpp
数据类型 数组名[常量表达式];

其中,“数据类型”用于指定数组中元素的数据类型;“数组名”就是数组的名字;“常量表达式”定义数组中存放的数据元素个数,就是数组的长度。

cpp
int a [10]; // 声明一个整型数组,包含10个元素
char b [128]; // 声明一个字符型数组,包含128个元素
  1. 数组名的命名必须符合标识符命名规范。
  2. 数组名后面的括号是方括号,方括号内是数组的长度。
  3. 数组大小不能动态定义,必须是一个常量。

1.2 一维数组元素的引用

一维数组元素的引用形式为“数组名[下标]”。

cpp
#include<iostream>
using namespace std;
int main() {
	int x[3] = { 1, 2, 3 };
	cout << x[0] << x[1] << x[2] << endl;
	return 0;
}

提示

数组元素的下标是从0开始的,而不是1。对于数组x,x[3]会造成下标越界,造成意外的错误。

1.3 一维数组的初始化

数组元素的初始化有两种:一种是逐个赋值,另一种是聚合赋值。

1.3.1 逐个赋值

cpp
#include<iostream>
using namespace std;
int main() {
  int x[3];
  x[0] = 3;
  x[1] = 4;
  x[2] = 5;

  cout << x[0] << x[1] << x[2] << endl;
  return 0;
}

1.3.2 聚合方式赋值

除了逐个进行赋值之外,还可以通过大括号同时对多个数组元素进行赋值。

  1. 如果只给部分数组元素赋值,未被赋值的元素默认被赋值为0。
  2. 当对所有元素都赋初始值时,可以不指定数组长度。
cpp
#include<iostream>
using namespace std;
int main() {
	// 1. 局部聚合赋值
	double x[3] = { 1.0, 2.0 };
	cout << "x0:" << x[0] << " x2:" << x[2] << endl;

	// 2. 全局聚合赋值
	char y[] = { 'A', 'B', 'C' };
	cout << y[0] << y[1] << y[2] << endl;
	return 0;
}

2. 二维数组

2.1 二维数组声明

一维数组描述的是一个线性序列,二维数组描述的是一个矩阵。二维数组包含行和列两个维度,a[m][n]表示一个包含m行n列的二维数组。

cpp
a[常量表达式1][常量表达式2];

其中表达式1表示行数,表达式2表示列数。

  1. 数组名的命名必须符合标识符命名规则。
  2. 二维数组有行和列两个下标,因此声明时需要两个方括号。
  3. 常量表达式表示数组的长度,不能是变量,因此数组的大小不能动态定义。
  4. 数组中每一维度的长度必须是正整数,其乘积决定了整个数组的元素个数。

2.2 二维数组元素的引用

二维数组的引用形式为“数组名[行下标][列下标]”。

引用二维数组元素时,需要注意:

  1. 行下标、列下标的索引都是从0开始的。一个m行n列的二维数组,其行下标的取值范围是0~m-1,列下标的取值范围是0~n-1。
  2. 二维数组在内存中时按行存放元素的。例如,数组a[3][4]在内存中先存放a[0]行,包含a[0][0]、a[0][1]、a[0][2]、a[0][3]……。

2.3 二维数组的初始化

二维数组元素的初始化方式和一维数组相同,也分为逐个赋值和聚合赋值。

cpp
int a [2][3];
// 逐个赋值
a[0][0] = 1;
a[0][1] = 2;
a[0][2] = 3;
a[1][0] = 4;
a[1][1] = 5;
a[1][2] = 6;
//聚合赋值
int b [3][2] = {{1,2}, {3,4}, {5, 6}};
// 或
int c [3][2] = {1,2,3,4,5,6};

3. 字符数组

字符数组中,一个元素存放一个字符。

3.1 字符数组的声明与初始化

声明方式:

cpp
char str[11];

为该字符数组赋值。

cpp
char str[11];
str[0] = 'H';
str[1] = 'E';
str[2] = 'L';
str[3] = 'L';
str[4] = 'O';
str[5] = ' ';
str[6] = 'W';
str[7] = 'O';
str[8] = 'R';
str[9] = 'L';
str[10] = 'D';
// 聚合方式
char str1[5] = {'H', 'E', 'L', 'L', 'O'};

提示

不能用字符数组给另一个字符数组赋值。

cpp
char a[5] = {'H', 'E', 'L', 'L', 'O'};
char b[5];
b = a; // 错误,不能这样赋值
b[0] = a[0]; // 正确

3.2 字符串

字符数组常用于存储字符串,此时要连同字符串结束符\0一起保存。

可以使用字符串直接给字符数组赋值。

cpp
char a[] = "Hello World";
// 等同于
char b[] = "Hello World\0";

字符串结束符\0的作用告诉编译器该字符串已经结束,不需要再访问内存。

字符数组与字符串的本质区别就是是否包含结束字符\0。字符数组中,其元素可以存放任何字符,并不要求最后一个字符必须是\0。但作为字符串使用时,就必须以\0结束,缺少这一标志时,系统虽然不一定会报错,但这种潜在的错误可能导致严重后果。因为在字符串的处理过程中,系统在遇到字符串结束符之前,会一直向后访问,以致会超出分配给字符串的内存空间,或者访问到其他数据所在的存储单元。

4. 指针

要想弄明白指针,要明白数据是如何存储又是如何被读取的,通常来说,系统会按字节对每个内存单元进行编号,这些内存单元就好比是许多带编号的小房间,要想使用内存,就需要知道这些房间的编号。在定义数据的时候,系统会将数据的内容存放到一个个的房间中,我们要想找到这些数据,就需要知道房间对应的编号,房间的编号就可以当做数据存放的地址,通过地址就可以找到相应的数据。

因此可以说房间的地址“指向”了这些定义的数据,因此地址被形象化地称为该数据(变量)的指针,意思是通过它就可以找到对应的内存单元。

4.1 指针变量

一个变量的地址称为该变量的指针。如果有一个变量,专门用来存放另一个变量的地址,那就是指针变量。C++中有专门来存放内存单元地址的变量类型,就是指针类型。

指针变量可以像普通变量一样声明、赋值和引用。

1.指针的声明:

cpp
数据类型 *指针变量名

其中*表示该变量是一个指针变量,数据类型表示该指针指向的变量的数据类型。

cpp
int *p;
float *p1;

2.指针的赋值

与普通变量赋值不同,给指针变量只能赋变量的地址,不能是其他的数据类型,C++中一般使用&来获取某个变量的地址,称为取地址符。

cpp
int i = 100;
int *p = &i;

提示

注意,变量的实际地址编写代码时是无法知晓的,当程序运行时系统会为变量分配内存空间,此时才能获取变量的内存地址。

警告

没有初始化的指针变量称为野指针。野指针并非不能使用,但是有一定的危害(可能指向不合法的内存空间),良好的习惯是在定义指针变量的时将其初始化为null,使其暂时指向空值。

3.指针变量的引用

引用指针变量的形式为*指针变量,其含义是引用指针变量所指向地址内的值。

通过变量名访问一个变量是直接的,而通过指针访问一个变量是间接的,先找到地址,然后再获取变量的值。

4.指针说明

指针的变量名是p,而不是*p

小栗子: 定义一个指针并赋值,然后输出指针指向的地址。

cpp
#include<iostream>
using namespace std;
int main() {
	int x = 10;
	int* p_x = &x;
	cout << p_x << endl;
	printf("%d\n", p_x);
	return 0;
}

指针变量不能直接赋值。

cpp
int *p = 100; // 这样是不可以的,编译直接报错

不能将*p当做变量使用。

cpp
int x = 100;
int *p;
*p = 100; // 这是错误的

4.2 指针运算符和取地址运算符

1.“*”和“&”的区别:

“&”是取地址运算符,作用是获取某个变量的内存地址。

“*”是指针运算符,作用是获取某个地址内变量的值。

小栗子:

cpp
#include<iostream>
using namespace std;
int main() {
	int x = 10;
	int *p_x = &x;
	cout << "x:" << x << endl;
	cout << "*p_x:" << *p_x << endl;
	return 0;
}

最终输出的结果都是10;

2.“&*”和“&”区别:

“&”运算符和“*”运算符的优先级相同,按自右向左的方式。

  1. &*p 先进行进行 “*”运算,*p相当于变量x;再进行“&”运算,“&*p”相当于取变量x的地址。
  2. *&x 先进进行“&x”运算,获取变量的地址;再进行“*”运算,获取变量的值。

注意: &*p中的p只能是指针变量,如果将*放到普通变量名前,编译器会出现逻辑错误。

cpp
int x = 10;
int *p;
printf("%d\n", &*x); // 非法指向错误,x不是指针变量

4.3 指针的自增和自减

指针变量存储的是地址,因此对指针做运算就等于对地址做运算。

小栗子: 定义指针变量和整型变量,并将整型变量的地址赋值给指针变量,再进行指针自增、自减运算。

cpp
#include<iostream>
using namespace std;
int main() {
	int x = 10;
	int* p_x = &x;
	p_x++;
	printf("address:%d\n", p_x);
	p_x--;
	printf("address:%d\n", p_x);
	return 0;
}

指针进行一次自增或自减运算,地址不是减少一个字节而是四个字节,这是因为指针的数据类型位整型,而整型的字节长度为四个字节。

4.4 指向空的指针与空指针类型

为了避免野指针危害,可以先将指针指向空值NULL。

cpp
int *p = NULL;

除此之外,指针本身还可以是任意类型,包括空类型(void)。

cpp
void *p;

空类型(void)的含义是指针变量指向的地址内可以存放任意数据类型。具体要存放那种类型,可以通过之后的赋值确定。赋值后,还需要将其强制转化为对应的数据类型,才能得到正确的结果。

小栗子: 定义一个整型指针并赋空值NULL,再定义一个空类型指针,然后尝试将不同类型的变量赋给它,并在输出时进行强制类型转化。

cpp
#include<iostream>
using namespace std;
int main() {
	int *pi = NULL; 
	int i = 4;
	float f = 1.22F;
	bool b = true;
	void* pv = NULL;
	cout << "开始赋值..." << endl;
	pv = pi;
	// cout << *(int*)pv << endl; // 被赋空的指针无法被使用,直到被赋到其他的值。
	pv = &i;
	cout << *(int*)pv << endl;
	pv = &f;
	cout << *(float*)pv << endl;
	pv = &b;
	cout << *(bool*)pv << endl;
	return 0;
}

4.5 指向常量的指针与指针常量

C++中,const是常量限定符,通过const关键字可以把一个变量转成常量。

cpp
const int x = 10;  // 通过const关键字定义的x变量,后续不允许再修改其值。

const除了可以修饰 int char float外还可以修饰指针,使指针指向一个常量,或使指针本身成为一个常量。

1.指向常量的指针

如果指针指向的是一个常量,则后续不能通过引用指针(*p)改变所指向的内容。

cpp
int x = 10;
const int *p = &x; // 定义p为指针常量。
*p = 3; // 错我,不能通过*p修改指针所指向的内容。

2.指针常量

指针常量,表示该指针是一个常量,指针指向一个内容之后,不能在指向其他内容。(本质是指针指向的地址不能发生改变)。

cpp
int x = 10;
const int y = 20;
int *const p_x = &x; // 指针无法再指向其他地址。
const int * p_y = &y; // 指针无法指向其他地址,并且指针指向的地址所对应的值也不能发生改变。

3.两者的区别

如何区分两者呢?可以根据“*”和const关键字的位置来判断。

cpp
const int *p1 = &x; // 这是一个指向整型常量的指针。
const int * const p1 = &x; // 这是一个指针常量,指向一个整型常量。
  • 指向常量的指针,指针本身可能不是一个常量,但指向的类型是一个常量,说明无法通过修改指针修改其值。
  • 指针常量,说明指针本身就是一个常量,不能修改指针指向的地址。

5. 指针与数组

5.1 数组的存储方式

数组作为同类型元素的有序集合,被顺序存放在一块连续的内存中,且每个元素的存储空间大小相同。数组中第一个元素存储地址就是数组的首地址,该地址存放在数组名中。

一维数组的结构是线性的,数组元素按下标由小到大的顺序依次存放在一块连续的内存中。二维数组以矩阵方式,先行后列依次存放元素,在内存中仍然是线性结构。

5.2 指针与一维数组

指针是存放地址的变量,如果把数组的首地址赋值给指针,就可以通过指针访问数组的元素了。

cpp
#include<iostream>
using namespace std;
int main() {
	int arr[5] = {1,2,3,4,5};
	int* p = &arr[0];
	for (int i = 0; i < 5; ++i) {
		cout << *p++ << endl;
	}
	return 0;
}

5.3 指针与二维数组

同理,将二维数组的首地址赋值给指针,同样可以访问数组元素。

cpp
#include<iostream>
using namespace std;
int main() {
	int arr[2][3] = {
		{1,3,4},
		{2,3,5}
	};
	int* p = arr[0];

	for (int i = 0; i < 6; ++i) {
		cout << *p++ << endl;
	}
	return 0;
}

5.4 指针与字符数组

cpp
#include<iostream>
using namespace std;
int main() {
	char str[] = "hello world";
	char* p = &str[0];
	for (int i = 0; i < 11; ++i) {
		cout << *p++ << endl;
	}
	return 0;
}

6. 指针在函数中的应用

之前接触到的函数都是按照值传递参数的,实参传递给函数体之后在函数内部使用的是实参的副本,在函数内改变副本是不会影响到实参的。

指针作为函数实参时,实参传递给函数后同样会生成指针变量的副本,但副本指针与原始指针是指向同一块内存空间的,因此改变指针副本指向的内容,就会改变原始指针指向的内容。

小栗子: 经典交换两个变量的值

cpp
#include<iostream>
using namespace std;
void swap(int* x, int* y);
int main() {
	int x = 10;
	int y = 20;
	swap(&x, &y);
	cout << x << ' ' << y << endl;
	return 0;
}

void swap(int* x, int* y) {
	int temp;
	temp = *x, *x = *y, *y = temp;
}

6.1 指向函数的指针

指针变量也可以指向函数。函数在编译时会被分配一个入口地址,使用指针变量指向该函数,后续通过指针调用此函数,形式如下:

cpp
int x, y; // 定义两个整型变量
(*f)(x, y); // 调用指针f指向的函数,x,y为函数参数

小栗子: 完成一个函数求两个值的平均值(假设参数都是可以整除的整型)。

普通函数求平均值:

cpp
#include<iostream>
using namespace std;
int avg(int x, int y);
int main() {
	int x = 10;
	int y = 20;
	int res = avg(10, 20);
	cout << res;
	return 0;
}

int avg(int x, int y) {
	return (x + y) / 2;
}

使用指针函数求平均值:

cpp
#include<iostream>
using namespace std;
int avg(int x, int y);
int main() {
	int x = 10;
	int y = 20;
	int (*pF)(int, int) = avg;
	cout << (*pF)(x, y);
	return 0;
}

int avg(int x, int y) {
	return (x + y) / 2;
}

6.2 空指针调用函数

空类型指针指向任意类型的函数,或者将任意类型的函数指向赋值空指针类型,都是合法的。使用空类型指针调用自身指向的函数,仍然按照强制转化的形式使用。

小栗子: 定义一个加法函数,在主函数定义一个指针类型,将其初始化为NULL,再用指针做实参调用加法函数。

cpp
#include<iostream>
using namespace std;
int pluss(int x) {
	return x + 1;
}
int main() {
	void* p = NULL;
	p = pluss;
	int result = ((int(*)(int))p)(10);
	cout << result;
	return 0;
}

警告

当函数被重载时,不要使用直接将函数名赋给空类型指针的操作,这会使编译器无法确定将那个重载函数交给空类型指针。

6.3 函数返回值为指针

函数的返回值也可以是指针,这样的函数称为指针函数。

函数返回的是一个地址。

小栗子:

cpp
#include<iostream>
using namespace std;
int* pAvg(int x, int y) {
	int result = (x + y) / 2;
	return &result;
}
int main() {
	int* p = pAvg(10, 20);
	cout << *p << endl;
	return 0;
}

小提示

值为NULL的指针地址是0,并不意味着这块内存可以使用。将指针赋值为NULL是基于安全考虑。

7. 指针数组

数组中的元素均为指针变量的数组称为指针数组。

cpp
类型名 *数组名[数组长度];

小栗子:

cpp
#include<iostream>
using namespace std;
int main() {
	int x = 10, y = 20, z = 30;
	int* p[3] = {&x, &y, &z};
	for (int i = 0; i < 3; ++i) {
		cout << *p[i] << endl;
	}
	return 0;
}

8. 安全使用指针

8.1 内存分配

1.堆与栈

在程序中定义一个变量,其值会被放到内存中,变量的值到底被存放到哪里呢?

如果未向系统申请动态内存,变量的值会被放到栈中。在栈中,变量占用的内存大小是无法被改变的,它们的占用与释放与变量定义的位置和存储方式有关。

与栈相对应,堆是一种动态内存。当向系统申请动态分配内存,变量将会被放到堆中,根据需要,这个变量的内存大小是可以改变的,内存申请和释放由开发者决定。

2.关键字 new 与 delete

C++中,new用于申请动态堆内存空间,delete用于释放堆内存空间。

小栗子:申请动态内存:

cpp
#include<iostream>
using namespace std;
int main() {
	int* p1 = new int; // 定义指针并申请一块动态内存
	*p1 = 111; // 将111放到堆内存空间中
	int x = 10; // 定义一个变量10, 10将放到栈内存中
	cout << p1 << endl;
	cout << &x << endl;
	return 0;
}

小栗子:释放内存:

cpp
#include<iostream>
using namespace std;
int main() {
	int* p1 = new int; // 定义指针并申请一块动态内存
	*p1 = 111; // 将111放到堆内存空间中
	delete p1; // 释放堆内存
	cout << p1 << endl;
	return 0;
}

💬 欢迎评论!请确保您已登录 GitHub。