数组、指针和引用
数组是一组相同类型数据的集合,指针是可以操作内存数据的变量,引用是变量的别名。数组的首地址可以看做是指针,通过指针可以操作数组,指针和引用在函数参数传递中可以相互替代。指针是一把双刃剑,使用得好能够带来效率的提升,使用不当也会给程序带来意想不到的灾难。
1. 一维数组
1.1 一维数组声明
程序设计中,将一组数据类型相同的数据按一定形式有序组织起来,这样的线性序列称为数组。每个数组都有一个数组名,通过数组名和下标可以唯一确定一个数组元素。
数据类型 数组名[常量表达式];
其中,“数据类型”用于指定数组中元素的数据类型;“数组名”就是数组的名字;“常量表达式”定义数组中存放的数据元素个数,就是数组的长度。
int a [10]; // 声明一个整型数组,包含10个元素
char b [128]; // 声明一个字符型数组,包含128个元素
- 数组名的命名必须符合标识符命名规范。
- 数组名后面的括号是方括号,方括号内是数组的长度。
- 数组大小不能动态定义,必须是一个常量。
1.2 一维数组元素的引用
一维数组元素的引用形式为“数组名[下标]”。
#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 逐个赋值
#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 聚合方式赋值
除了逐个进行赋值之外,还可以通过大括号同时对多个数组元素进行赋值。
- 如果只给部分数组元素赋值,未被赋值的元素默认被赋值为0。
- 当对所有元素都赋初始值时,可以不指定数组长度。
#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列的二维数组。
a[常量表达式1][常量表达式2];
其中表达式1表示行数,表达式2表示列数。
- 数组名的命名必须符合标识符命名规则。
- 二维数组有行和列两个下标,因此声明时需要两个方括号。
- 常量表达式表示数组的长度,不能是变量,因此数组的大小不能动态定义。
- 数组中每一维度的长度必须是正整数,其乘积决定了整个数组的元素个数。
2.2 二维数组元素的引用
二维数组的引用形式为“数组名[行下标][列下标]”。
引用二维数组元素时,需要注意:
- 行下标、列下标的索引都是从0开始的。一个m行n列的二维数组,其行下标的取值范围是0~m-1,列下标的取值范围是0~n-1。
- 二维数组在内存中时按行存放元素的。例如,数组a[3][4]在内存中先存放a[0]行,包含a[0][0]、a[0][1]、a[0][2]、a[0][3]……。
2.3 二维数组的初始化
二维数组元素的初始化方式和一维数组相同,也分为逐个赋值和聚合赋值。
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 字符数组的声明与初始化
声明方式:
char str[11];
为该字符数组赋值。
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'};
提示
不能用字符数组给另一个字符数组赋值。
char a[5] = {'H', 'E', 'L', 'L', 'O'};
char b[5];
b = a; // 错误,不能这样赋值
b[0] = a[0]; // 正确
3.2 字符串
字符数组常用于存储字符串,此时要连同字符串结束符\0
一起保存。
可以使用字符串直接给字符数组赋值。
char a[] = "Hello World";
// 等同于
char b[] = "Hello World\0";
字符串结束符\0
的作用告诉编译器该字符串已经结束,不需要再访问内存。
字符数组与字符串的本质区别就是是否包含结束字符\0
。字符数组中,其元素可以存放任何字符,并不要求最后一个字符必须是\0
。但作为字符串使用时,就必须以\0
结束,缺少这一标志时,系统虽然不一定会报错,但这种潜在的错误可能导致严重后果。因为在字符串的处理过程中,系统在遇到字符串结束符之前,会一直向后访问,以致会超出分配给字符串的内存空间,或者访问到其他数据所在的存储单元。
4. 指针
要想弄明白指针,要明白数据是如何存储又是如何被读取的,通常来说,系统会按字节对每个内存单元进行编号,这些内存单元就好比是许多带编号的小房间,要想使用内存,就需要知道这些房间的编号。在定义数据的时候,系统会将数据的内容存放到一个个的房间中,我们要想找到这些数据,就需要知道房间对应的编号,房间的编号就可以当做数据存放的地址,通过地址就可以找到相应的数据。
因此可以说房间的地址“指向”了这些定义的数据,因此地址被形象化地称为该数据(变量)的指针,意思是通过它就可以找到对应的内存单元。
4.1 指针变量
一个变量的地址称为该变量的指针。如果有一个变量,专门用来存放另一个变量的地址,那就是指针变量。C++中有专门来存放内存单元地址的变量类型,就是指针类型。
指针变量可以像普通变量一样声明、赋值和引用。
1.指针的声明:
数据类型 *指针变量名
其中*
表示该变量是一个指针变量,数据类型
表示该指针指向的变量的数据类型。
int *p;
float *p1;
2.指针的赋值
与普通变量赋值不同,给指针变量只能赋变量的地址,不能是其他的数据类型,C++中一般使用&
来获取某个变量的地址,称为取地址符。
int i = 100;
int *p = &i;
提示
注意,变量的实际地址编写代码时是无法知晓的,当程序运行时系统会为变量分配内存空间,此时才能获取变量的内存地址。
警告
没有初始化的指针变量称为野指针
。野指针并非不能使用,但是有一定的危害(可能指向不合法的内存空间),良好的习惯是在定义指针变量的时将其初始化为null,使其暂时指向空值。
3.指针变量的引用
引用指针变量的形式为*指针变量
,其含义是引用指针变量所指向地址内的值。
通过变量名访问一个变量是直接的,而通过指针访问一个变量是间接的,先找到地址,然后再获取变量的值。
4.指针说明
指针的变量名是p
,而不是*p
。
小栗子: 定义一个指针并赋值,然后输出指针指向的地址。
#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;
}
指针变量不能直接赋值。
int *p = 100; // 这样是不可以的,编译直接报错
不能将*p
当做变量使用。
int x = 100;
int *p;
*p = 100; // 这是错误的
4.2 指针运算符和取地址运算符
1.“*”和“&”的区别:
“&”是取地址运算符,作用是获取某个变量的内存地址。
“*”是指针运算符,作用是获取某个地址内变量的值。
小栗子:
#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.“&*”和“&”区别:
“&”运算符和“*”运算符的优先级相同,按自右向左的方式。
- &*p 先进行进行 “*”运算,*p相当于变量x;再进行“&”运算,“&*p”相当于取变量x的地址。
- *&x 先进进行“&x”运算,获取变量的地址;再进行“*”运算,获取变量的值。
注意: &*p中的p只能是指针变量,如果将*放到普通变量名前,编译器会出现逻辑错误。
int x = 10;
int *p;
printf("%d\n", &*x); // 非法指向错误,x不是指针变量
4.3 指针的自增和自减
指针变量存储的是地址,因此对指针做运算就等于对地址做运算。
小栗子: 定义指针变量和整型变量,并将整型变量的地址赋值给指针变量,再进行指针自增、自减运算。
#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。
int *p = NULL;
除此之外,指针本身还可以是任意类型,包括空类型(void)。
void *p;
空类型(void)的含义是指针变量指向的地址内可以存放任意数据类型。具体要存放那种类型,可以通过之后的赋值确定。赋值后,还需要将其强制转化为对应的数据类型,才能得到正确的结果。
小栗子: 定义一个整型指针并赋空值NULL,再定义一个空类型指针,然后尝试将不同类型的变量赋给它,并在输出时进行强制类型转化。
#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关键字可以把一个变量转成常量。
const int x = 10; // 通过const关键字定义的x变量,后续不允许再修改其值。
const除了可以修饰 int char float外还可以修饰指针,使指针指向一个常量,或使指针本身成为一个常量。
1.指向常量的指针
如果指针指向的是一个常量,则后续不能通过引用指针(*p)改变所指向的内容。
int x = 10;
const int *p = &x; // 定义p为指针常量。
*p = 3; // 错我,不能通过*p修改指针所指向的内容。
2.指针常量
指针常量,表示该指针是一个常量,指针指向一个内容之后,不能在指向其他内容。(本质是指针指向的地址不能发生改变)。
int x = 10;
const int y = 20;
int *const p_x = &x; // 指针无法再指向其他地址。
const int * p_y = &y; // 指针无法指向其他地址,并且指针指向的地址所对应的值也不能发生改变。
3.两者的区别
如何区分两者呢?可以根据“*”和const关键字的位置来判断。
const int *p1 = &x; // 这是一个指向整型常量的指针。
const int * const p1 = &x; // 这是一个指针常量,指向一个整型常量。
- 指向常量的指针,指针本身可能不是一个常量,但指向的类型是一个常量,说明无法通过修改指针修改其值。
- 指针常量,说明指针本身就是一个常量,不能修改指针指向的地址。
5. 指针与数组
5.1 数组的存储方式
数组作为同类型元素的有序集合,被顺序存放在一块连续的内存中,且每个元素的存储空间大小相同。数组中第一个元素存储地址就是数组的首地址,该地址存放在数组名中。
一维数组的结构是线性的,数组元素按下标由小到大的顺序依次存放在一块连续的内存中。二维数组以矩阵方式,先行后列依次存放元素,在内存中仍然是线性结构。
5.2 指针与一维数组
指针是存放地址的变量,如果把数组的首地址赋值给指针,就可以通过指针访问数组的元素了。
#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 指针与二维数组
同理,将二维数组的首地址赋值给指针,同样可以访问数组元素。
#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 指针与字符数组
#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. 指针在函数中的应用
之前接触到的函数都是按照值传递参数的,实参传递给函数体之后在函数内部使用的是实参的副本,在函数内改变副本是不会影响到实参的。
指针作为函数实参时,实参传递给函数后同样会生成指针变量的副本,但副本指针与原始指针是指向同一块内存空间的,因此改变指针副本指向的内容,就会改变原始指针指向的内容。
小栗子: 经典交换两个变量的值
#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 指向函数的指针
指针变量也可以指向函数。函数在编译时会被分配一个入口地址,使用指针变量指向该函数,后续通过指针调用此函数,形式如下:
int x, y; // 定义两个整型变量
(*f)(x, y); // 调用指针f指向的函数,x,y为函数参数
小栗子: 完成一个函数求两个值的平均值(假设参数都是可以整除的整型)。
普通函数求平均值:
#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;
}
使用指针函数求平均值:
#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,再用指针做实参调用加法函数。
#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 函数返回值为指针
函数的返回值也可以是指针,这样的函数称为指针函数。
函数返回的是一个地址。
小栗子:
#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. 指针数组
数组中的元素均为指针变量的数组称为指针数组。
类型名 *数组名[数组长度];
小栗子:
#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用于释放堆内存空间。
小栗子:申请动态内存:
#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;
}
小栗子:释放内存:
#include<iostream>
using namespace std;
int main() {
int* p1 = new int; // 定义指针并申请一块动态内存
*p1 = 111; // 将111放到堆内存空间中
delete p1; // 释放堆内存
cout << p1 << endl;
return 0;
}
💬 欢迎评论!请确保您已登录 GitHub。