C/C++数组和指针学习总结

39 篇文章 14 订阅
6 篇文章 2 订阅

指针和数组的区别与联系

  • 数组是用于存储多个相同类型数据的集合
  • 指针是一个存储内存地址的变量(或数据对象)
  • 使用sizeof:
    – 指针的所占的内存空间为4字节(32位)或8字节(64位)
    – 对数组使用sizeof,返回的是整个内存块的大小,即元素个数*元素所占内存,数组作为参数传递时,数组名退化成了一个指针,使用sizeof返回的是指针的大小4或8。

与指针相关的运算符

  • 地址运算符 &
    后跟一个变量名时,&给出该变量的地址。&a表示变量a的地址
  • 地址运算符 *
    后跟一个指针名或地址时,* 给出存储在指针地址上的值。
    *b=a;//表示把a指向的地址上的值赋给b。

声明指针

声明指针变量时,必须指定指针所指向变量的类型,因为不同的变量类型占用不同的存储空间。一些指针操作需要知道操作对象的大小。另外,程序必须知道储存在指定地址上的数据类型。

int * pi;//pi是指向int类型变量的指针

//使用指针解决交换函数的问题 
#include<stdio.h>
//void interchange(int * u,int * v);
//可以省略ANSI C风格的函数原型中的形参名:
void interchange(int *, int *); 

int main(void)
{
	int x = 5,y = 10;
	printf("Originally x = %d and y = %d.\n",x,y);
	interchange(&x,&y); //把地址发送给函数
	printf("Now x = %d and y = %d.\n",x,y);
	
	return 0; 
} 
void interchange(int * u, int * v)
{
	int temp;
	temp = *u;
	*u = *v;//*u表示x的值,*v表示y的值, *u = *v相当于 x = y 
	*v = temp;
}

变量:名称、地址和值

编写程序时,可认为变量有两个属性:名称和值。
计算机编译和加载时,也认为变量有两个属性:地址和值。地址就是变量在计算及内部的名称。

//数组学习
#include<stdio.h>
int main(void){
	const int days[] = {31,28,31,30,31,30,31,31,30,31};
	int index;
	//sizeof 是以字节为单位返回对象的大小, 一个int是四个字节大小,数组内部有10个int类型的变量,所以数组days的大小为40 
	printf("%d\n",sizeof(int));
	printf("%d\n",sizeof days);
	printf("%d\n",sizeof days[0]);
	//整个数组的大小除以单个元素的大小就是数组元素的个数 
	for (index = 0; index < sizeof days / sizeof days[0]; index++)
		printf("Month %2d has %3d days.\n",index + 1,days[index]);	
	return 0;
} 

指定初始化器(C99)

C99增加了一个新特性:指定初始化器。利用该特性可以初始化指定的数组元素。例如只初始化数组中的最后一个元素。

给数组赋值

  • C 不允许把数组作为一个单元赋给另一个数组
  • 除了初始化以外不允许使用花括号列表的形式赋值。
  • 只有在定义数组时才能使用初始化
// 一些错误的数组赋值
int oxen[5] = {5,3,2,1};// 初始化没问题
int yaks[5];

yaks = oxen;  //不允许
yaks[5] = oxen[5]; //数组下标越界
yaks[5] = {5,3,2,6}; //不起作用
//oxen数组的最后一个元素为oxen[4],所以oxen[5]和yaks[5]都超出了两个数组的末尾。

C++11的列表初始化

int a[]{1,2,3,4};//初始化数组时可以省略等号
double b[12]{};//把所有元素设为0

数组边界

在使用数组时,要防止数组下标越界,也就是要确保下标是有效的值。

	int arr1[6] = {0,0,0,0,0,212} ;//数组下标应为0~5
	printf("%d\n",arr1[99]);//但编译器不会报错。

在C标准中,使用越界下标的结果是未定义的。因为信任程序员,不检查边界,提高运行速度。

指针和数组

//指针地址
#include<stdio.h>
#define SIZE 4
int main(void){
	int dates[SIZE];
	int * pti;
	int index;
	double bills[SIZE];
	double * ptf;
	pti = dates;//把数组地址赋给指针 
	ptf = bills;
	printf("23s %15s\n","short","double");
	for (index = 0; index < SIZE; index++)
		printf("pointers + %d: %p %p\n", index, pti + index, ptf + index);
	return 0; 
} 

结果:

                    int          double
pointers + 0: 000000000024FE20 000000000024FE00
pointers + 1: 000000000024FE24 000000000024FE08
pointers + 2: 000000000024FE28 000000000024FE10
pointers + 3: 000000000024FE2C 000000000024FE18

地址按字节编址,short类型占用2字节,int占8字节,double占8字节。在C中,指针加一指的是增加一个储存单元,对数组而言,这意味着加一后的地址是下一个元素的地址。而不是下一个字节的地址。这也是为什么必须声明指针所指向的数据类型的原因之一。

  • 指针的值是他所指向对象的地址,地址的表示方式依赖计算及内部的硬件。许多计算机都是按字节编址,意思是内存中的每一个字节都是按顺序编号,一个较大对象的地址(如double类型的变量)通常是该对象第一个字节的地址。
  • 在指针前面使用*运算符可以得到该指针所指向对象的值。
  • 指针加一,指针的值递增它所指向类型的大小(以字节为单位)。
dates + 2 == &date[2] //相同的地址
*(dates + 2) == dates[2] //相同的值

函数、数组和指针

#include<stdio.h>
#define SIZE 10
//以下四种函数声明等价 
int sum(int ar[], int n);
//int sum(int *ar,int n);
//int sum (int *, int);
//int sum(int [], int);
int main(void){
	int a = 1;
	 printf("%u bytes.\n",sizeof &a);	
	int marbles[SIZE] = {20,10,5,6,8,99,77,88,66,31};
	long answer;//出于安全考虑,求和后的结果可能会内存溢出,所以用long类型 
	answer = sum(marbles,SIZE);
	printf("The total number of marbles is %ld.\n", answer);
	printf("The size of marbles is %u bytes.\n",sizeof marbles);	
	return 0; 
} 
//另一种函数定义:
//int sum(int *ar, int n){}  
int sum(int ar[], int n){
	int i;
	int total = 0;
	for(i = 0; i < n; i++)
		total += ar[i];	
	//传入的是个指针,ar并不是数组本身,它是一个指向marbles数组首元素的指针 
	//为什么是 8 bytes, 因为我们的系统用8字节存储地址,所以指针变量的大小是8 字节(其他系统可能不是用8字节保存地址) 
	printf("The size of ar is %u bytes.\n", sizeof ar);
	printf("The value of ar[0] is %d.\n", *ar);
	return total;
}
/*
结果:
The size of ar is 8 bytes.
The value of ar[0] is 20.
The total number of marbles is 410.
The size of marbles is 40 bytes.
*/
*/
/*指针运算中的优先级*/
#include<stdio.h>
int data[2] = {100,200};
int moredata[2] = {300,400};
int main(void){
	int *p1, *p2,*p3;
	p1 = p2 = data;
	p3 = moredata;
	printf(" *p1   = %d, *p2   = %d, *p3     = %d\n",*p1, *p2, *p3);
	printf(" *p1++ = %d, *++p2 = %d, (*p3)++ = %d\n",*p1++, *++p2, (*p3)++);
	printf(" *p1   = %d, *p2   = %d, *p3     = %d\n",*p1, *p2, *p3);
	return 0;
} 
结果:
 *p1   = 100, *p2   = 100, *p3     = 300
 *p1++ = 100, *++p2 = 200, (*p3)++ = 300
 *p1   = 200, *p2   = 200, *p3     = 301

指针操作

/*指针操作*/
#include<stdio.h>
int main(void){
	
	int urn[5] = { 100, 200, 300, 400, 500};
	int * p1, *p2, *p3;
	p1 = urn;// 把一个地址赋给指针 
	p2 = &urn[2];//也是把一个地址赋给指针 
	
	printf("pointer value, dereferenced pointer, pointer address:\n");
	//p1是个指针变量,它的值是urn的首元素的地址,所以打印p1展示的是urn[0]的地址 
	//*p1是把p1的值,也就是p1所指向的地址上的值取出来,也就是urn[0]的值
	//因为指针也是个变量,&p1就是把p1的地址取出来, 
	printf("&urn[0] = %p, p1 = %p, *p1 = %d, &p1 = %p\n",&urn[0], p1,*p1, &p1);
	printf("\n********************************************************************************\n");
	//指针加法
	p3 = p1 + 4;
	printf("\nadding an int to a pointer:\n");
	printf("&urn[4] = %p, urn[4] = %d\n", &urn[4], urn[4]);
	printf("p1 + 4 = %p, *(p1+4) = %d\n", p1 + 4, *(p1 + 4));
	printf("\n***********************************************************************************\n");
	
	p1++;
	printf("\nvalues after p1++:\n");
	//p1++就是指向下一个存储地址,也就是urn[1],p1不管是++还是--改变的是它的值,而不是p1的地址,所以&p1不会变 
	printf("&urn[1] = %p, urn[1] = %d\n", &urn[1], urn[1]);
	printf("p1 = %p, *p1 = %d, &p1 = %p\n", p1, *p1, &p1);
	printf("\n***********************************************************************************\n");
	
	p2--;
	printf("\nvalues after p2--:\n");
	//p2--是指向上一个存储地址,也就是urn[1] 
	printf("p2 = %p, *p2 = %d, &p2 = %p\n", p2,*p2, &p2);	
	--p1;//恢复为初始值 
	++p2;//恢复为初始值 
	printf("\n***********************************************************************************\n");	
	printf("\nPointers reset to original values:\n");
	printf("&urn[1] = %p, &urn[2] = %p\n", &urn[0], &urn[2]);
	printf("p1      = %p, p2       = %p\n",p1,p2); 
	return 0;
} 

指针变量的基本操作:

  • 赋值:可以把地址赋给指针。注意地址应该和指针类型兼容,比如不能把double类型的地址赋给指向int的指针。
  • 解引用:*运算符给出指针指向地址上储存的值。
  • 取址:和所有变量一样,指针变量也有自己的地址和值。对指针而言,&运算符给出指针本身的地址。
  • 指针与整数相加:整数都会和指针所指向对象的大小(以字节为单位)相乘,然后把结果和初始地址相加。如果相加的结果超出了初始指针所指向的数组范围,计算结果则是未定义的。除非正好超过数组末尾的第一个位置,C保证该指针有效。
  • 递增指针:递增指向数组元素的指针可以让该指针移动至数组的下一个元素。
  • 递减指针:与递增同理。
  • 指针减去一个整数:同整数相加同理,不过指针必须是第一个运算对象,整数是第2的运算对象,如p1-3,而不是3-p1.
  • 指针求差:可以计算两个指针的差值。通常求差的两个指针分别指向同一个数组的不同元素,计算两元素的距离。要求是都指向同一数组。
  • 比较:使用关系运算符可以比较两个指针的值,前提是都指向同类型的对象。
    注意事项:
    不要解引用未初始化的指针
	int * pt;//未初始化的指针 
	*pt = 5;//严重的错误 

创建一个指针时,系统只分配了储存指针本身的内存,并未分配储存数据的内存,因此在使用指针之前,必须先用已分配的地址初始化它。

保护数组中的数据

编写一个处理基本类型(如,int)的函数时,可以选择是传递int类型的值还是传递指向int的指针。通常是直接传递数值,只有程序需要在函数中改变该数值时才传递指针。对于数组,则必须传递指针,因为效率高。
但传递地址会导致一些问题,处理数组的函数通常都需要使用原始数据,这样的函数会修改原始数组,但是有时我们并不希望修改原数组,
为了解决该问题,可以在函数形参中使用const关键字:

int sum(const int ar[], int n);

const关键字告诉编译器,该函数不能修改ar指向的数组中的内容。这样使用const关键字并不是要求原数组为常量,而是该函数在处理数组时要将其视为常量,不可更改。

指针和多维数组

//指针和多维数组
#include<stdio.h>
int main(void){
	int zippo[4][2] = {{2,4},{5,8},{9,78},{56,40}};
	/*
		因为数组名zippo是数组首元素的地址,所以zippo的值和  &zippo[0] 的值相同。而zippo[0]本身也是个数组
		所以zippo[0]的值和它首元素的地址(即 &zippo[0][0]的值)相同,
		zippo[0]是一个占用一个int大小对象的地址,而zippo是一个占用两个int大小对象的地址。
		由于这个整数和内含两个整数的数组都开始于同一个地址,所以zippo和zippo[0]的地址相同。
		(对于数组,取地址可以省去 & 符合) 
	*/	
	printf("   zippo = %p,    zippo[0] = %p,    zippo[0][0] = %p\n", zippo, zippo[0], &zippo[0][0]);
	/*
	运行结果: 
		   zippo = 000000000024FE30,    zippo[0] = 000000000024FE30,    zippo[0][0] = 000000000024FE30
	*/
	printf("\n ******************************************************\n");
	 /*
	 	由于zippo是一个指向占用两个int大小对象的地址,所以zippo + 1会跳过两个地址,与zippo[1] 的地址相同
		 而zippo[0]是指向占用一个int大小对象的地址,所以zippo[0] + 1会指向zippo[0][1]的地址。 
	 */ 
	printf("zippo + 1 = %p,   zippo[0] + 1 = %p\n",zippo + 1, zippo[0] + 1);
	printf("zippo[1] = %p,   zippo[0][1] = %p\n",zippo[1], &zippo[0][1]);
	printf("\n ******************************************************\n");
	/*
		*zippo代表该数组首元素(zippo[0])的值,但是zippo[0]本身是一个int类型值的地址。 
		该值的地址是 &zippo[0][0],所以*zippo就是&zippo[0][0]; 而*zippo + 1 则是 &zippo[0][1]
	*/
	printf("*zippo = %p,    *zippo + 1 = %p\n",*zippo, *zippo + 1);
	printf("\n ******************************************************\n");	
	/*
		由于*zippo等价于&zippo[0][0],所以**zippo等价于*&zippo[0][0]
		而zippo[0]也等价于 &zippo[0][0],所以 *zippo也等价于*&zippo[0][0]
	*/
	printf("zippo[0][0] = %d,    *zippo[0] = %d,    **zippo = %d\n", zippo[0][0], *zippo[0],**zippo);
	printf("\n ******************************************************\n");	
	printf("zippo[2][1] = %d,    *(*(zippo+2) + 1) = %d\n", zippo[2][1], *(*(zippo+2) + 1));	
	
	return 0;
} 

指向多维数组的指针

//指向多维数组的指针
#include<stdio.h>
int main(void){
	int zippo[4][2] = {{2,4},{5,8},{9,78},{56,40}};
	/*
		pz指向一个内含两个int类型的数组
		pz为指向一个数组的指针,该数组内含两个int类型值,
		为什么要在声明中使用括号?因为[]的优先级高于*,考虑下面的声明:
		int * pax[2]; //pax是一个内含两个指针元素的数组,每个元素都指向int的指针
		由于[]优先级更高,先与pax结合,所以pax成为一个内含两个元素的数组,然后 * 表示pax数组内含两个空指针,
		最后,int表示pax数组中的指针都指向int类型的值。 
	*/
	int (*pz)[2];
	pz = zippo;
	printf("pz = %p,    pz + 1 = %p\n", pz, pz + 1);
	printf("pz[0] = %p, pz[0] + 1 = %p\n", pz[0],pz[0] + 1);
	printf("*pz = %p,  *pz + 1 = %p\n",*pz,*pz+1);
	printf("pz[0][0] = %d\n",pz[0][0]);
	printf("*pz[0] = %d\n",pz[0][0]);
	printf("**pz = %d\n",**pz);
	printf("pz[2][1] = %d\n",pz[2][1]);
	printf("*(*(pz+2) + 1) = %d\n",*(*(pz + 2) + 1));
	return 0;
} 

函数和多维数组

//函数和多维数组
#include<stdio.h>
#define ROWS 3
#define COLS 4
void sum_rows(int arr[][COLS], int rows);
void sum_cols(int [][COLS], int); //省略形参名,没问题
int sum2d(int(*arr)[COLS], int rows);
int main(void){
	int junk[ROWS][COLS] = {
		{2,4,6,8},
		{3,5,7,9},
		{12,10,8,6}
	}; 
	
	sum_rows(junk,ROWS);
	sum_cols(junk,ROWS);
	printf("Sum of all elements = %d\n",sum2d(junk,ROWS));
	
	return 0;
} 

void sum_rows(int arr[][COLS], int rows){
	int r;
	int c;
	int tot;
	printf("%d",arr[0][0]);
	for(r = 0; r < rows; r++){
		tot = 0;
		for( c = 0; c < COLS;c++)
			tot += arr[r][c];	
		printf("row %d: sum = %d\n", r, tot);
		
	}
}

void sum_cols(int arr[][COLS], int rows){
	int r;
	int c;
	int tot;
	for(c = 0; c < COLS; c++){
		tot = 0;
		for(r = 0; r < rows;r++)
			tot += arr[r][c];
		printf("col %d: sum = %d\n",c, tot);
	}
}

int sum2d(int arr[][COLS], int rows){
	int r;
	int c;
	int tot = 0;
	for(r = 0; r < rows; r++){
		for(c = 0; c < COLS;c++)
			tot += arr[r][c];
	}
	return tot;
}

注意,下面的声明不正确:

int sum(int arr[][], int rows);//错误的声明

编译器会把数组表示法转换成指针表示法,例如,编译器会把arr[1]转换成arr + 1.编译器对arr +1求值,要知道ar所指向的对象的大小。

int sum(int arr[][4], int rows);//有效的声明

表示arr指向一个内含4个int类型值的数组,,如果第二个方括号是空的,编译器就不知道怎么处理了,也可以在第一对方括号中写上大小,但是编译器会忽略该值。

void指针

无类型指针:void *pi,也指向内存地址,但不指定这个地址单元内的数据类型。
不可以直接赋值给其他类型的指针;访问内存数据时,必须进行强制转换,才可以间接访问内存数据。
不会单独使用,只是作为指针类型转换的中介。比如:通过内存区域的复制函数:memcpy()。原理:将某种类型数据的地址转换 void 指针,进行复制后,再强制转换为原理的地址类型。

由于 void 指针没有特定的类型,因此它可以指向任何类型的数据。也就是说,任何类型的指针都可以直接赋值给 void 指针,而无需进行其他相关的强制类型转换。
如:

void *p1;
int *p2;
p1 = p2;

但这并不意味着可以无需任何强制类型转换就将 void 指针直接赋给其他类型的指针:

	void *p1;
	int *p2;
	p2 = p1;//错误
	p2 = (int *)p1;//正确

如果函数的参数可以是任意类型指针,应该将其参数声明为 void*


```cpp
void printM(int ** mat) {
	for (int i = 0; i < 4; i++)
	{
		for (int j = 0; j < 5; j++)
		{
			cout << mat[i][j] << " ";
		}
		cout << "" << endl;
	}
}
void initM(int ** mat) {
	for (int i = 0; i < 4; i++)
	{
		for (int j = 0; j < 5; j++)
		{
			mat[i][j]  = i+j;
		}
		
	}
}
	int a[4];//只声明,未初始化
	double b[5] = {};//声明并初始化,所有元素均为0
	char c[5] = {'a'};//声明并初始化,第一个元素为'a',其余元素为'\0'
	cout << "size a = "<<sizeof a << endl;
	cout << "size b = "<<sizeof b << endl;
	cout << "size c = "<<sizeof c << endl;
	//size a = 16
	//size b = 40
	//size c = 5
	// 
	
	//指向二维数组的指针
	int **mat = new int *[4];//指向指针的指针,创建一个存放指针的数组
	for (int i = 0; i < 4; i++) {
		mat[i] = new int[5];//给数组中的每个指针分配5个int内存块,只是分配了内存,并未初始化
	}
	
	printM(mat);
	initM(mat);
	printM(mat);
/*
		-842150451 - 842150451 - 842150451 - 842150451 - 842150451
		- 842150451 - 842150451 - 842150451 - 842150451 - 842150451
		- 842150451 - 842150451 - 842150451 - 842150451 - 842150451
		- 842150451 - 842150451 - 842150451 - 842150451 - 842150451
		0 1 2 3 4
		1 2 3 4 5
		2 3 4 5 6
		3 4 5 6 7
		*/

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

SOC罗三炮

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

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

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

打赏作者

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

抵扣说明:

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

余额充值