目录
一、内存和地址
二、指针是什么?
三、指针变量的内容
四、指针类型
五、间接访问操作符
(一)易混淆
六、野指针
(一)野指针成因
1. 指针未初始化
2. 指针越界访问
3. 指针指向的空间释放
(二)如何规避野指针
(三)处理未初始化指针
1. 总是用NULL来初始化指针
2. 用assert函数
补充:指针为什么要初始化
七、常量和指针
(一)指向常量的指针
1. 具体形式
2. 把pci声明为指向整数常量的指针意味着:
(二)指向非常量的常量指针
1. 举例
2. 注意
(三)指向常量的常量指针
八、指针的指针
(一)对于二级指针的运算有:
(二)注意:
(三)总结
九、字符指针
(一)易错 :cout特性
(二) 把字符串 hello,world! 首字符的地址放到p中
(三)举例
1. 常量字符串之间
2. 字符数组之间
3. 总结
十、指针数组
(一)整形数组和字符数组
(二)整形指针数组
十一、数组指针
(一)判断数组指针
举例1: 定义一个数组指针,该指针指向char* arr[5]数组
举例2: 定义一个数组指针,该指针指向int arr2[10]数组
(二)&数组名VS数组名
十二、 数组参数、指针参数
(一)一维数组传参
1. 举例
(二)二维数组传参
1. 传参方式
2. 举例
(三)一级指针传参
1. 当一个函数的参数部分为一级指针的时候,函数能接收什么参数?
2. 举例
(四)二级指针传参
1. 当函数的参数为二级指针的时候,可以接收什么参数?
2. 举例
一、内存和地址
- 此处有五个整数,每个数都对应一个存储地址
- 但是要记住地址太笨拙,所以高级语言所提供的特性之一是通过名字而非地址来访问内存的位置。下面这张图与上图相同,但这次使用名字来代替地址
- 这些名字就是我们所称的变量,注意名字和内存位置之间的关联不是硬件所提供,它是由编译器为我们实现的。所有这些变量给了我们一种更方便的方法记住地址------硬件仍然通过地址访问内存位置
二、指针是什么?
- 在计算机科学中,指针(Pointer)是编程语言中的一个对象,利用地址,它的值直接指向(point to)存在电脑存储器中另一个地方的值,由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址的内存单元。
- 简而言之,指针就是变量,用来存放地址的变量(存放在指针中的值都被当成地址处理)
- 指针的大小在32位平台是4个字节,在64位平台是8个字节
三、指针变量的内容
int a = 112, b = -1;在内存中开辟空间
float c = 3.14;
int* d = &a;//这里对变量a,取出它的地址
//将a的地址存放在d变量中,d就是一个指针变量
float* e = &c;
- a的值是112,b的值是-1,c的值是3.14,d的值是100,e的值是108
- 注意:d的值不是112,e不是3.14,因为变量的值就是分配给该变量的内存位置所存储的数值,即使是指针变量也不以外!
- 指针变量的赋值只能赋予地址
四、指针类型
- 变量有不同的类型,整形,浮点型等,指针也有相应的类型
例如:
char *pc = NULL;
int *pi = NULL;
short *ps = NULL;
long *pl = NULL;
float *pf = NULL;
double *pd = NULL;
- char* 类型的指针是为了存放 char 类型变量的地址
- short* 类型的指针是为了存放 short 类型变量的地址
- int* 类型的指针是为了存放 int 类型变量的地址
五、间接访问操作符
- 通过一个指针访问它所指向的地址的过程称为间接访问或解引用指针,这个用于执行间接访问的操作符是单目操作符*
表达式 | 右值 | 类型 |
a | 112 | int |
b | -1 | int |
c | 3.14 | float |
d | 100 | int* |
e | 108 | float* |
*d | 112 | int |
*e | 3.14 | float |
- d的值是100,*d表示访问内存位置100 并查看那里的值,即*d=112
(一)易混淆
- *&a=25表达什么意思?
- 先取a的地址,然后利用*访问地址,向位置(&a)的空间赋值25,也就是a=25
六、野指针
- 概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
(一)野指针成因
1. 指针未初始化
错误写法:
正确:
#include <iostream>
#include<cstdlib>
using namespace std;
int main()
{
int* p = (int*)malloc(sizeof(int));
if (p == NULL)
exit(-1);
*p = 20;
cout << *p;//20
free(p);
return 0;
}
2. 指针越界访问
//错误写法
#include <iostream>
using namespace std;
int main()
{
int arr[10] = { 0 };
int* p = arr;
int i = 0;
for (i = 0; i <= 11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
3. 指针指向的空间释放
(二)如何规避野指针
- 指针初始化
- 小心指针越界
- 指针指向空间释放即使置NULL
- 避免返回局部变量的地址
- 指针使用之前检查有效性
(三)处理未初始化指针
1. 总是用NULL来初始化指针
- 把指针初始化为NULL更熔炉检查是否使用正确,即便这样,检查空值也比较麻烦
int* p = NULL;
......
if (p == NULL)//不应该解引p
{
}
else//可以使用p
{
}
2. 用assert函数
- 下面代码测试了p变量是否为空值,如果表达式为真,那么什么都不会发生;如果表达式为假,程序会终止
assert(p != NULL);
补充:指针为什么要初始化
- 如果变量没有被初始化,包含的都是垃圾数据,所谓垃圾是指,分配的内存中可能包含任何数据。当内存刚分配时不会被处理,之前的内容可能是任何东西。如果之前的内容是一个浮点数,但是现在需要存放的是整数,将一个浮点数当成一个整数没有什么用,就算确实包含了整数,也不大可能是正确的整数,所有说内容是垃圾
- 只有初始化后,指针才会正常工作
七、常量和指针
(一)指向常量的指针
- 将指针定义为指向常量,这意味着不能通过指针修改它所引用的值 (指针认为自己指向的是常量,所有不允许用指针来修改这个常量)
1. 具体形式
const int *pci;//指向整数常量
//等价形式int const* pci;
2. 把pci声明为指向整数常量的指针意味着:
- pci可以被修改为指向不同的整数常量
- pci可以被修改为指向不同的非整数常量
- 可以解引用pci以读取数据
- 不能解引用pci从而修改它所指向的数据
#include <iostream>
#include<cstdlib>
using namespace std;
int main()
{
int num = 5;
const int limit = 500;
int* pi = (int*)malloc(sizeof(int));//指向整数
const int* pci = (int*)malloc(sizeof(int));//指向整数常量
cout << &pci << " " << pci << endl;//000000ACB338FC98 00000269CB403140
pci = &limit;//对应第一点
cout << &pci << " " << pci << endl;//000000ACB338FC98 000000ACB338FC54
pci = #//对应第二点
cout << &pci << " " << pci << endl;//000000ACB338FC98 000000ACB338FC34
//*pci=123;是错误的
free(pi);
return 0;
}
(二)指向非常量的常量指针
- 指针不可变化,但是指向的数据可变
1. 举例
int num = 5;
int *const cpi=#
2. 注意
- cpi必须被初始化为指向非常量变量
- cpi不能被修改
- cpi指向的数据可以被修改
#include <iostream>
using namespace std;
int main()
{
int num = 5;
const int limit = 25;
int *const cpi=#
cout << cpi << " " << *cpi<<endl;//000000B56B6FFA04 5
*cpi = limit;
cout << cpi << " " << *cpi << endl;//000000B56B6FFA04 25
//cpi = &limit;错误写法,常量指针不能修改
return 0;
}
(三)指向常量的常量指针
- 指针不能修改,指针指向的数据也不能修改
- 可以将常量的地址赋值给cpci
#include <iostream>
using namespace std;
int main()
{
int num = 5;
const int limit = 25;
//const int *const cpci;错误写法必须初始化
const int* const cpci = &limit;
cout << cpci << " " << *cpci << endl;//000000CF749EF9D4 25
const int* const cpci2 = #
cout << cpci2 << " " << *cpci2 << endl;//000000CF749EF9B4 5
return 0;
}
八、指针的指针
- 指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里?
- 这就是 二级指针
int a=12;
int* pa=&a;
int** ppa=&pa;
- 变量pa是一个指向整型的指针,所以ppa的类型是一个指向“整型的指针”的指针,也就是指针的指针
- a的地址存放在pa中,pa的地址存放在ppa中
(一)对于二级指针的运算有:
- *ppa 通过对ppa中的地址进行解引用,这样找到的是 pa , *ppa 其实访问的就是 pa
- **ppa 先通过 *ppa 找到 pa ,然后对 pa 进行解引用操作: *pa ,那找到的是 a
#include <iostream>
using namespace std;
int main()
{
int a = 12;
int* pa = &a;
int** ppa = &pa;
cout << &ppa << " " << ppa << endl;//000000C434FEF9F8 000000C434FEF9D8
cout <<&pa << " " << pa << endl;//000000C434FEF9D8 000000C434FEF9B4
cout <<&a<<" "<< a;//000000C434FEF9B4 12
return 0;
}
(二)注意:
- *具有从右至左的结合性
(三)总结
表达式 | 相当的表达式 |
a | 12 |
pa | &a |
*pa | a,12 |
ppa | &pa |
*ppa | pa,&a |
**ppa | *pa,a,12 |
九、字符指针
(一)易错 :cout特性
- cout输出的结尾条件是找到‘\0’,但是字符并没有‘\0’,所有cout会以该字符为第一个字符开始持续打印,并强行将随后的地址当作后续字符,打印出乱码
(二) 把字符串 hello,world! 首字符的地址放到p中
#include<iostream>
using namespace std;
int main()
{
const char* p = "hello,world!";
cout << &p << " " << p;//000000CC043AFAE8 hello,world!
return 0;
}
(三)举例
1. 常量字符串之间
#include<iostream>
using namespace std;
int main()
{
const char* p = "hello,world!";
const char* p2 = "hello,world!";
if (p == p2)
cout << "p和p2相同" << endl;
else
cout << "p和p2不同" << endl;
return 0;
}
原因:
2. 字符数组之间
#include<iostream>
using namespace std;
int main()
{
char str[]= "hello,world!";
char str2[] = "hello,world!";
if (str == str2)
cout << "str和str2相同" << endl;
else
cout << "str和str2不同" << endl;
return 0;
}
原因:
3. 总结
C/C++会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块
十、指针数组
- 指针数组是存放指针的数组
(一)整形数组和字符数组
(二)整形指针数组
int*arr3[5]
十一、数组指针
- 数组指针是指针?还是数组?答案是:指针
- 数组指针:能够指向数组的指针
int* p;//p是整形指针----指向整形的指针----可以存放整形的地址
char* pc;//pc是字符指针----指向字符的指针----可以存放字符的地址
- 同理,数组指针----指向数组的指针----存放数组的地址
(一)判断数组指针
- 下面代码哪个是数组指针?
int *p1[10];
int (*p2)[10];
- 第一个:p1先和[ ]结合 ,有10个元素,每个元素的类型是int*
- 第二个是数组指针,p2先和*结合,说明p2是一个指针变量,然后指向的是一个大小为10个整型的数组。所以p2是一个指针,指向一个数组,叫数组指针
- 注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合
举例1: 定义一个数组指针,该指针指向char* arr[5]数组
char* arr[5];
char* (*pa)[5] = &arr;
举例2: 定义一个数组指针,该指针指向int arr2[10]数组
int arr2[10];
int (*p)[10] = &arr2;
(二)&数组名VS数组名
#include<iostream>
using namespace std;
int main()
{
int arr[10];
cout << arr << " " << arr+1 << endl;//000000F84C1AF798 000000F84C1AF79C
cout << &arr << " " << &arr+1 << endl;//000000F84C1AF798 000000F84C1AF7C0
return 0;
}
- arr是数组名,数组名表示数组首元素的地址
- &arr 表示的是数组的地址,而不是数组首元素的地址。本例中 &arr 的类型是: int(*)[10] ,是一种数组指针类型数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40
十二、 数组参数、指针参数
(一)一维数组传参
- 数组名的值就是一个指向数组第一个元素的指针,此时传递给函数的是一份该指针的拷贝,函数如果执行了下标引用(例如arr[0],arr[1]....),实际上是对这个指针执行间接访问操作,并且通过这种间接访问,函数可以访问和修改调用程序的数组元素
1. 举例
#include<iostream>
using namespace std;
void test1(int a[])
{
swap(a[0],a[1]);
}
void test2(int b[10])
{
swap(b[2], b[3]);
}
void test3(int* c)
{
swap(c[4], c[5]);
}
void test4(int* d[])
{
swap(d[0], d[1]);
}
void test5(int* e[20])
{
swap(e[2], e[3]);
}
void test6(int** f)
{
swap(f[4], f[5]);
}
void print(int arr[])
{
for (int i = 0; i < 6; i++)
{
cout << arr[i] << " ";
}
cout << endl<<endl;
}
void print2(int* arr2[])
{
for (int i = 0; i < 6; i++)
{
cout << arr2[i] << " ";
}
cout << endl << endl;
}
int main()
{
int arr[] = { 11,15,13,67,89,45 };
int* arr2[] = {&arr[0],&arr[1],&arr[2],&arr[3],&arr[4],&arr[5]};
print(arr);//11 15 13 67 89 45
test1(arr);
print(arr);
test2(arr);
print(arr);
test3(arr);
print(arr);
print2(arr2);
test4(arr2);
print2(arr2);
test5(arr2);
print2(arr2);
test6(arr2);
print2(arr2);
return 0;
}
(二)二维数组传参
- 二维数组:使用行和列标识数组元素,这类数组需要映射为内存中的一维地址空间
- 数组名表示首元素地址,二维数组的首元素是第一行,首元素的地址是第一行的地址,而二维数组的第一行是一个一维数组,故首元素的地址是一个一维数组的地址
1. 传参方式
void test1(int a[row][col])//第一种
void test2(int b[][col])//第一种
void test3(int (*c)[col])//第二种
错误形式:
void test(int* arr);//参数定义一个整形指针,是存放整形数据
void test(int** arr);//参数是二级指针,二级指针是存放一级指针的地址
- 注意:二维数组传参,函数形参的设计只能省略第一个[]的数字。因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素
2. 举例
#include<iostream>
using namespace std;
void test1(int a[3][5])
{
a[0][0] = 1, a[0][1] = 2;
}
void test2(int b[][5])
{
b[1][0] = 3, b[1][1] = 4;
}
void test3(int(*c)[5])
{
c[1][0] = 5, c[1][1] = 6;
}
void print(int d[3][5])
{
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 5; j++)
{
cout << d[i][j] << " ";
}
cout << endl;
}
}
int main()
{
int arr[3][5] = { 0 };
print(arr);
test1(arr);
cout << endl;
print(arr);
test2(arr);
cout << endl;
print(arr);
test3(arr);
cout << endl;
print(arr);
return 0;
}
(三)一级指针传参
1. 当一个函数的参数部分为一级指针的时候,函数能接收什么参数?
例如:
void test1(int *p);//p是一级指针
- 可以接收地址或者一级指针
2. 举例
#include<iostream>
using namespace std;
void test(int* p)
{
;
}
int main()
{
int a = 12;
int* p1 = &a;
test(&a);//传地址
test(p1);//传一级指针
return 0;
}
(四)二级指针传参
1. 当函数的参数为二级指针的时候,可以接收什么参数?
- 可以接收一级指针的地址和二级指针
2. 举例
#include<iostream>
using namespace std;
void test(int** p)
{
;
}
int main()
{
int a = 12;
int* p = &a;
int** pp = &p;
test(&p);//传一级指针的地址
test(pp);//传二级指针
int* arr[10];//arr是指针数组,里面存放的指针,传arr过去实际是传首元素的地址过去(首元素是一个指针),也就是传入了一个指针的地址
test(arr);
return 0;
}