指针和数组的区别与联系
- 数组是用于存储多个相同类型数据的集合
- 指针是一个存储内存地址的变量(或数据对象)
- 使用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
*/