一、数组
1. 定义
数组是一组相同类型元素的集合,它在内存中是连续存放的。创建方式为:
type_t arr_name [const_n]
,如:
int arr[5]
char arr[3]
double arr[10]
2.初始化:
- 不完全初始化:
int arr[5]={1,2,3}
剩下的元素默认为0; - 未指定数组长度:
int arr[]={1,2,3,4}
- 字符串形式初始化:
char arr[]='abcd'
补充:sizeof和strlen
sizeof:“sizeof()”运算符求的是字符数组的长度,而不是字符串长度。只跟你给该字符串数组定义了多大空间有关,而跟字符串是否结束无关.如果遇到字符串,编译时会自动在末尾增加一个 null 字符,即char arr1[]='abc\0'。
strlen:用来计算以’\0’结尾的字符串长度的函数。它并不是计算内存大小,仅计算字符串从开端到’\0’结尾字符的个数(不包含’\0’)。
char arr1[]='abc';//字符串
char arr2[]={'a','b','c'}//字符数组
sizeof(arr1)=4//41=4, char arr[]={'a','b','c','\0'}
sizeof(arr2)=3//31=3
strlen(arr1)=3
strlen(arr2)=随机数
其中,arr1[]是字符串,arr2[]是字符数组
总结:以字符串形式出现的,编译器都会为该字符串自动添加一个0作为结束符,如在代码中写 "abc",那么编译器帮你存储的是"abc/0",char arr[]="abc"实际上存储的是 char arr[]={'a','b','c','\0'}
3.二维数组
3.1 创建方式
数据类型 数组名称[行][列],如:int arr[3][2]代表三行两列的数组
3.2 初始化
- 不完全初始化:
int arr[3][2]={1,2,3}
剩下的元素默认为0;
1 2
3 0
0 0 - 指定行列:
int arr[3][4]=={{1,2,3},{4,5}}
1 2 3 0
4 5 0 0
0 0 0 0
3.3 使用
访问元素:
for(int i=0;i<3;i++){
for(int j=0;j<4;j++){
print("%d",arr[i][j]);
}
print("\n")
}
数组作为函数参数:
void bubble_sort(int arr[],int sz){
{
...
}
int main(){
int arr[]={1,2,3,4,5};
bubble_sort(arr,sz);//我们对arr进行传参,实际上传递过去的是数组的首元素的地址即&arr[0];
int sz=sizeof(arr)/sizeof(arr[0]);
...
return 0;
}
补充1:关于sizeof(arr)/sizeof(arr[0])
sizeof(arr)计算的是数组arr所占的总字节数,即空间大小;
sizeof(arr[0])是单个元素的大小;
sizeof(arr)/sizeof(arr[0])就是数组的长度;
如:int arr[]={1,2,3,4,5}
数组长度:sizeof(arr)/sizeof(arr[0])
其中,整数 int占4个字节,总字节数/4就是数组长度;
char arr[]={'a','b','c'}
数组长度:sizeof(arr)/sizeof(arr[0])
其字母占1个字节,故可简写成:sizeof(arr)。
补充2:&数组名、&数组名【】
&数组名:取出的是整个数组的地址(打印出首元素地址作为整个地址地代表)->p=&arr,*p=arr(p是整个数组的地址,*p是数组首元素的地址)
&数组名[0]:取出的是数组的首元素地址
数组名:取出的是数组的首元素地址
☀️☀️注意:
①数组名arr是首元素地址,但是以下两种 情况除外:
- sizeof(arr) 数组名表示整个数组,求得数组的大小,单位是字节。sizeof(arr)/sizeof(arr[0]);arr表示整个数组,sizeof(arr)表示整个数组的大小。
- &arr表示整个数组的地址。
②int arr[10]={0}
二、指针
1.1 定义
指针是编程语言中的一个对象,利用地址,它的值直接指向存在电脑存储器中的另一个地方的值,地址指向变量单元,存放地址的变量就是指针变量,换句话说,指针就是一个变量,里面存放着地址,指针就是地址。
如:
int a=10
int *p=&a
//p是一个指针变量
prunt(*p)
// *
是解引用,取指针p指向的地址里的内容,*p=10
指针的大小在32位平台是四个字节,在64位平台是八个字节。
1.2 指针和指针类型
(1)指针类型的意义
①指针类型决定了指针进行解引用操作的时候,能够访问空间的大小。
- int*p: *p能够访问4个字节
- char
*
p:*p能够访问1个字节 - double
*
p:*p能够访问8个字节 - ②指针类型决定了指针走一步走多远(指针的步长)
- int*p: p+1-->往后4字节
- char *p:p+1-->往后1字节
- double* p:p+1-->往后8字节
(2)野指针
指针执行的位置是不可知的
导致野指针的原因:
- 未初始化,局部变量不初始化,默认是随机值
- 指针越界访问
- 指针指向的空间释放
怎么避免野指针:
- 指针初始化
- 小心指针越界
- 指针指向空间释放的话,使之置为NULL
- 指针使用之前检查有效性
补充:
① i++与++i
区别一:i++是右值,++i是左值,左值是可以放到赋值符号左边的变量,即具有对应的可以由用户访问的存储单元,并且能够由用户去改变其值的量,而右值i++不可以。比如说:
int i=0;
++i=1;//正确
i++=1;//错误
左值与右值的根本区别在于是否允许取地址&运算符获得对应的内存地址,左值允许,右值不允许。如
&(++i)//正确
& (i++) //错误
为什么++i允许,而i++不允许呢?
C/C++语言中可以放在赋值符号左边的变量,即具有对应的可以由用户访问的存储单元,并且能够由用户去改变其值的量。左值表示存储在计算机内存的对象,而不是常量或计算的结果。或者说左值是代表一个内存地址值,并且通过这个内存地址,就可以对内存进行读并且写(主要是能写)操作;这也就是为什么左值可以被赋值的原因了。相对应的还有右值:当一个符号或者常量放在操作符右边的时候,计算机就读取他们的“右值”,也就是其代表的真实值。简单来说就是,左值相当于地址值,右值相当于数据值.
区别二:i++是先运算后自加;++i是先自加后运算。比如说:
i=3
n=i++,此时,n=3,i=4(先赋值运算,后加1)
n=++i,此时,n=4,i=4(先加1,后赋值运算)
②指针+-整数
float arr[5];
float *vp;//定义一个指针变量
for (vp=&arr[0]; vp<arr[5]; ){
. *vp++ = 0;
}
指针vp指向数组arr的首元素地址,vp++=0先赋值给vp为0,在vp+1指向第二个元素,第二个元素=0;直至第五个元素也为0.
③指针-指针(地址-地址)
必须是同类型指针
int arr[5]={1,2,3,4,5}
&arr[5]-&arr[0]=5//结果是两指针中间的元素个数
④指针比较大小
法一:
for(vp = &arr[5];vp>&arr[0]; ){
*--vp = 0;
}
法二:
for(vp = &arr[5-1];vp>=&arr[0];vp-- ){
*vp = 0;
}
但是更推荐第一种方法,标准规定:允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较即法一,不允许与指向第一个元素之前的那个内存位置的指针进行比较。
1.4 二级指针
1.4.1 定义
int a=10;
int * p1 = &a;//一级指针,int*分开,int表示p1指向的对象类型是int整形,*表示p1是指针
int* * p2=&p1//二级指针,int*表示p2指向的对象类型是int*指针即p1,右边的*表示p2是一个指针;
1.4.2 用法
解引用:
*p1=**p2=a=10
*p2=p1
1.4.3 指针与数组
#####(1)指针数组
指针数组就是存放指针的数组。
int a = 10;
int b = 20;
int c = 30;
int* pa=&a;
int* pb=&b;
int* pc=&c;
为了方便,我们可以将pa,pb,pc指针存放在一个数组中。
int* arr[3]={&a,&b,&c}
或int* arr[3]={pa,pb,pc}
遍历访问元素:
int i=0;
for(i=0;i<3,i++){
. *(arr[i])
}
(2)数组指针
存放数组的指针。见进阶
三、指针进阶
1.1 字符指针
1.1.1 定义
法一:
char ch = 'abc;
char* pc = &ch;
法二:
char* p = "abc"//把常量字符串“abc”的首元素a的地址放进了p中,而不是内容abc
这个严格来说应该这么写:const char* p="abc",理由后面介绍。
补充:字符数组和字符指针
(1)字符数组:
char arr1[4]="abcd"
char arr2[4]="abcd"
定义的是一个字符数组,所以就相当于定义了一些空间来存放"abcd",而又因为字符数组就是把字符一个一个地存放的,所以编译器把这个语句解析为 char arr[5] = {'a','b','c','d','\0'};
回顾之前到讲到的,sizeof(arr[5])=5; 扩展一下,如果char arr[] = "abcd"是在函数内部写的话,那么这里 的"abcd/0"因为不是常量,所以应该被放在栈上。
另外,arr1!=arr2,因为arr1,arr2分别定义了各自的空间来存储内容,这里恰巧两个的字符数组的内容一样而已。故,两者不一样。
(2)字符指针:
char* p1="abcd"
char* p2="abcd"
定义的是一个普通指针,并没有定义空间来存放"abcd",所以编译器得帮我们找地方来放"abcd",显然,把这里的"abcd"当成常量并把它放到程序 的常量区是编译器最合适的选择。拓展一下,
字符指针指向的字符串保存在内存的静态存储区中。
因为是常量字符串,如下操作:
char* p1="abcd"
p1=“h”
错误,常量字符串不可修改。
另外,p1==p2,因为p1,p2都是常量,内容都是“abcd”,都指向同一个内存空间。
此处,为避免错误,还是写成const char p="abcd"为好。
总结一下就是:
首先在内存的中位置不同,字符数组保存的字符串存放在内存的栈中,而字符指针指向的字符串保存在内存的静态存储区中。
其次字符数组保存的字符串属于字符串变量,可以被修改,而字符指针指向的字符串是属于字符串常量,不能被修改。
1.2 指针数组
1.2.1定义
指针数组是一个数组,用来存放指针。
int* p[10]={0}//存放整形指针的数组-指针数组
char* p[10]={0}//存放字符指针的数组-指针数组
1.2.2 使用
指针数组访问每个元素:
1.3 数组指针
数组指针是一个指针,(*p),用来指向数组的指针。
int* p=NULL //p是整形指针-指向整型的指针-存放整形的地址
char* pc=NULL //pc是字符指针-指向字符的指针-存放字符的指针
int (*p )[10]=&arr //数组指针-指向数组的指针-存放数组的地址
关于数组的地址,前面有讲过,即&arr。
书写方法:
char* arr[5];
char* (*p)[5]=&arr;
int arr2[10]={0];
int (*p2)[10]=&arr2;
补充:关于*
星号
①在定义变量时,代表着该变量是一个指针
int a=10;
int* p=&a;
②在取值操作时,叫解引用,即得到指针指向的地址的内容
*p=a=10
③ &放在一起,抵消掉,如:
int a,b; a=100; b=100; int *p,*q; p=&a; q=&b; *p=*q;
代入:&a=&b
抵消:a=b
④有,&符号出现,就说明此处用到了指针,指针(或者说数组/数组元素地址)的大小在32位平台是4,64位平台是8。原因:在32位cpu上,指针能够存储这2^32次个地址就需要4个字节。(1字节=8位).
遍历方法:
int arr[10]={1,2,3,4,5,6,7,8,9,10};
int* p=arr;
int i=0;
for(i=0;i<10;i++){
. printf("%d", *(p+i))
. printf("%d", *(arr+i))//用指针的方法打印
. printf("%d", arr[i])//普通的数组打印方式
. printf("%d",p[i])
四种打方式结果一样
}
关于二维数组:
二维数组的数组名是首元素的地址,这里的首元素不是第一行第一列的元素,而是第一行所有的元素。(这里把二维数组理解成特殊的一维数组)
遍历二维数组的元素:
法一:
参数是数组的形式
法二(用数组指针):
参数是指针的形式
难点:解释下为什么是*(*(p+i)+j):
补充:
*(*(p+i)+j)的等效写法:
①(*(p+i)[j])//备注:*
(p+i)=p[i],。*
(p+i)=p[i], 比如说,p是二维数组arr的首元素地址p=arr=&arr1,*
p=p[0]是一维数组arr1的首元素地址。
②*
(p[i]+j)
③p[i][j]
④二维数组的数组名是地址的地址a=&a[0]=&&a[0][0]
,一次解引用:*a=&a[0][0]
,二次解引用:**a=a[0][0]
a[0]讲解:
二维数组指针表示,C语言指针引用二维数组详解
1.4关于以上几种类型的总结
①int arr[5]//arr是一个5
个元素的整型数组
②int* parr1[10]//parr1是一个数组,数组有10个元素,每个元素的类型是int*, parr1是指针数组
③int(* parr2)[10]//parr2是一个指针,该指针指向了一个数组,该数组有10个元素,每个元素的类型是int, parr2是数组指针
④int (* parr3[10])[5])//parr3是一个数组,该数组有10个元素,每个元素是一个数组指针,该数组指针指向的数组有5个元素,每个元素的类型是int。
1.5 数组参数
1.5.1 一维数组
(1)数组在传参的时候可以将参数写成数组,也可以写成指针。如
void test(int arr[])
{ }
void test(int arr[10])
{ }
void test(int *arr);
{ }
int main(){
int arr[10]={0};
test(arr);
}
这三种传参都是正确的。
(2)指针数组在传参的时候可以将参数写成数组,也可以写成指针。如
void test(int* arr[])//数组类型是int*
{ }
void test(int* arr[10])
{ }
void test(int** arr);
{ }
int main(){
int* arr[10]={0};
test(arr);
return 0;
}
这三种传参也是正确的。
1.5.2 二维数组
(1)数组名写法
void test (int arr[3][5]) // 写成int arr[][5],不可以写成int arr[3][],行可以省略,列不可以省略
{ }
int main(){
int arr[3][5]={0};
test(arr);
return 0;
}
(2)指针写法
①void test (int* arr)//写法错误,整形指针只存放整形,不能存放数组,而arr是二维数组的首元素地址,也就是第一行数组的地址
{ }
②void test (int** arr)//写法错误,二级指针是用来存放一级指针的地址,而arr是一个数组的地址
{ }
③void test (int* arr[5])//写法错误,arr是一个数组,每个元素类型是int*
{ }
④void test (int(* arr)[5])//写法正确,arr是一个指针,指向第一行数组的五个元素,类型是int
{ }
int main(){
int arr[3][5]={0};
test(arr);
return 0;
}
1.6 指针传参
1.6.1 一级指针传参
void test1(int* p)//传过来的是地址(整形指针),所以这里要用一个指针来接收
{}
int main()
{
int a=10;
'test1(&a);//传过去的是地址
int* p=&a;
test1(p);//传过去的是a的地址,将a的地址存在指针变量p里面
}
1.6.2 二级指针传参
void test1(int** ptr)//传过来的是一级指针的地址,所以这里要用一个二级指针来接收
{}
int main()
{
int a=10;
int* p=&a;
int** pp=&p//pp是二级指针;
test1(pp);//传过去的是一级指针p的地址,将p的地址存在二级指针变量pp里面
test1(&p);
int* arr[10];//定义一个指针数组,里面存放着一级指针
test1(arr)//arr是数组首元素地址,也就是一级指针的地址
}
故,当函数的参数为二级指针得时候,参数可以是:
- 一级指针变量的地址
- 二级指针变量本身
- 存放一级指针的指针数组的数组名
1.7 函数指针
数组指针-指向数组的指针-存放数组的地址- int (* p)[10]
函数指针-指向函数的指针-存放函数的地址- int (* p)(in tx, int y)//函数指针类型int(* )(int x,int y),p是一个函数声明。
使用方法:
int ADD(int x,int y)
{ ...}
int main()
{
int(* p)(int x,int y);
p(2,3);
ADD(2,3);
(*p)(2,3)
(*ADD)(2,3)
//以上四种调用函数ADD的方法都正确
//调用的时候,的数量没有用,*没有意义
}
补充:
①&函数名和函数名都是函数的地址
②( *( void ( * )( ) ) 0 )( )
把0强制类型转换成:void()()函数指针类型,0就是一个函数的地址。(*(...)0)()调用0地址处的该函数。
③void (*signal(int , void(\*)(int) ) )( int )
signal是一个函数声明,signal的函数有2个参数,一个是int,一个是void(*)(int)函数指针,该函数指针指向的函数的参数类型是int,返回类型是void。
signal返回类型也是一个函数指针,该函数指针指向的函数的参数类型是int,返回类型是void。
该代码可以简化成:
typedef void(*pfun_t)(int);
pfun_t signal(int,pfun_t);
1.8 函数指针数组
指针数组-int* arr[5]
函数指针-int(* p)(int int)=ADD//函数指针的返回类型是int
函数指针数组-存放多个函数的地址即函数指针的地址-int (* parr[4])(int ,int)={ADD,SUB,MUL,DIV} : parr是一个数组,有四个元素,每个元素的类型是函数指针。
使用方法:
函数指针数组的用途--转移表:
计算器案列:
int main(){
}
//pfArr是一个函数指针数组,又叫转移表
1.9 指向函数指针数组的指针
1.9.1 定义
数组指针-指向数组的指针-存放数组的地址
指向函数指针的数组的指针--存放着函数指针数组的地址
int (* pf)(int , int)//函数指针
int(* pfArr[4])(int , int)//函数指针数组,pfArr是一个数组,函数指针数组
int(* (* ppfArr[4] )(int , int))//指向函数指针数组的指针,ppfArr是一个数组指针,指针指向的数组有4个元素,每个元素的类型是一个函数指针int( * )(int , int)
1.9.2 回调函数
回调函数就是一个通过函数指针调用的函数。
解释一下就是,如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
1.9.3 qsort函数
...
三、指针和数组笔试题
1.1 一维数组
1.1.1 sizeof()问题
(1)整形数组
(2)字符数组
(3)初始化为字符串的数组
总结:
一个指针(或者说数组/数组元素地址)的大小在32位平台是4,64位平台是8。原因:在32位cpu上,指针能够存储这2^32次个地址就需要4个字节。(1字节=8位),64位同理;
- 求地址的大小(数首元素地址,下一个元素地址,整个数组地址,下一个数组地址...)都是4/8字节。
- 求元素的大小,就看是整形还是字符,整形4个字节,字符型1个字节。
- 不分char,int等类型,只要是求地址大小,都是4/8.
1.1.2 strlen()问题
(1)字符数组
(2)初始化为字符串的数组
总结:
C 库函数 size_t strlen(const char *str) 接收的类型是地址char *,函数返回值是无符号的。从给定地址往后寻找,从给定直到空结束字符(不包括空结束字符),然后返回字符串 str 的长度。
如果传给strlen()的参数是未可知范围的地址,strlen会一直走下去,直到遇到"\0"为止,‘\0’出现位置是未知的,结果就是随机值(如char arr[]={'a','b','c'},strlen(arr) 随机);
如果传给strlen()的地址后面会出现'\0',那么就返回字符串的长度(char arr[]=“abc”,strlen(arr) =6);
如果传给strlen的参数是一个具体的元素而不是一个地址,这样会把字符a的ascii码值97传给strlen函数,而此函数是访问不到这个地址的,因此会程序中断。(char arr[]={'a','b','c'},strlen(*arr) 报错)。
1.2 二维数组
总结
①a[0]:
a[0]是二维数组的第一行,是二维数组的首元素地址,a[0]是第一行的数组名,也就是一维数组的首元素地址,即a[0]=&a[0][0]
二维数组指针表示,C语言指针引用二维数组详解
②a:
a是二维数组的数组名,没有sizeof(数组名),也没有&(数组名),所以a是二维数组的首元素地址,将二维数组看成一维数组,a就是第一行的地址即a=&a[0],又因为a[0]是a[0][0]的地址,有a == &(&a[0][0])
即二维数组名 a 是地址的地址。
③数组名的意义
数组名有:一维数组arr,二维数组a,二维数组的第i行a[i]
- sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节,元素个数x单个字节
- &数组名,表示是整个数组的地址
- 除此之外所有的数组名表示首元素地址
1.3 指针
题一:
*
(ptr-1)= ptr[-1]-->*
(*
(p+4)+2)=p[4][2]
题二:
题三:
题四:
补充:
逗号表达式:
a=(表达式1, 表达式2),先求解表达式 1,再求解表达式 2。整个逗号表达式的值是表达式 2 的值。
如:a=(1,2),a就是2;b=(count=19, incr=10, count+1),b就是20。
题五:
题六: