数组
数组的特点:
只能存放一种类型的数据,比如int类型的数组、float类型的数组
- 里面存放的数据称为“元素”
初始化方式
int a[] = {, , }; int a[] = {,}; int a[] = {, , }; int a[] = {[]=,[] = };
常见错误
int a[]; int[] a; int a[b]; a = {, }; a[] = {,,,};
内存分析
数组存储空间的大小
存储空间的划分(内存的分配是从高地址到低地址进行的,但一个数组内部元素又是从低到高进行的)
- 数组名的作用,查看元素地址
数组越界的注意
二维数组
二维数组是一个特殊的一维数组:它的元素是一维数组。例如int a[2][3]可以看作由一维数组a[0]和一维数组a[1]组成,这两个一维数组都包含了3个int类型的元素
初始化
int a[3][4] = {1,2,3,4,5,6};
int a[3][4] = {{},{},{}};
int a[][5] = {3,21,31,2,32,1};
注意错误:
int a[3][4];
a[3] = {};
字符串
很多个字符组合在一起就是字符串了
初始化
char a[] = “123”; 和 char a [] = {‘1’,’2’,’3’};的区别,可以比较大小
“123”其实是由’1’、’2’、’3’、’\0’组成
“123”的存储分布
字符串的输出”%s”,’\0’是不会输出的
\0的作用
'\0'是一般字符串语句中的结束符号,可以判断字符串是否结束
字符串数组
初始化
char names[][] = { {'J','a','y','\0'}, {'J','i','m','\0'} };
char names2[][] = { {"Jay"}, {"Jim"} };
char names3[][] = { "Jay", "Jim" };
指针
定义格式 类名标识符 *指针变量名; 如 int *p;
先定义后赋值
// 简单取值
int a = ;
int *p;
p = &a;
printf(“%d”, *p);
// 简单改值
*p = ;
定义的同时赋值
int a = ;
int *p = &a;
清空指针
p = ;
p = NULL;
指针与数组
1、指向数组元素的指针与普通指针变量一样,例如
int a[];
int *p=&a[];
p=a; //数组名代表数组的首地址
p=&a[];//指针指向第二个元素
在数组里面,通过*(p+1)、*(p-1)就可以直接获取上下元素。
2、指针下标:在C语言中,中括号[]是下标运算符,一般我们习惯用数组名[下标]对数组元素进行访问,实际上,下标运算符可用于指针运算,例如:
int a[];
int *p=&a[]; //指针p指向数组的第二个元素
p[1] --> *(p+1) 下一个元素,即a[2]
p[-1] --> *(p-1) 上一个元素,即a[0]
下标运算符:D[N]没有规定D和N的顺序,N[D]也是可以的,例如:
1[p] --> *(1+p)
(p+1)[1] --> *(p+1+1) //用的人少
3、访问数组元素的4种方式: a[i]、 *(a+i)、p[i]、*(p+i)
#include <stdio.h>
int main(){
int x[] = {,,};
int s=,i,*p=x+; //*p = &x[2]
for(i=;i>=;--i)
s -= (-i)[p]; // (-i)[p] = *(-i+p) 1-1=0 0-2=-2 -2-3=-5
printf("%d\n",s); // -5
return ; //1[p] --> *(1+p)
}
指针与常量
1、指向常量的指针:指针变量指向一个常量,此时指针所指向的值不能修改,但指针本身(存储的地址)可以指向另外的对象,例如
int a = , b = ;
const int *p = &a;
*p = ; //报错,不能修改
p = &b; //可以修改
2、指针常量:声明指针常量,则指针本身不能改变。例如
int a=, b=;
int *const p = &a;
p = &b; //报错,不能修改
*p = ; //可以修改
理解:看const后面是什么,如果是*p则表示指针的值不能修改,如果是p则表示指针不能更换指向。
3、类型转换:C语言中,如果将常量的地址赋值给非常量指针(类型转换),就可以修改地址里的值,从而影响代码中用到常量的表达式。例如
const int a=; //常量
int *p = (int*)&a; //类型换换,去掉const
*p = ; //修改地址指向的值
int b = a+; //101
指针与字符串
1、字符串除了用字符数组处理外,还可以用字符指针来处理,字符指针指向字符串常量(连续存储区)的首地址。例如
char str[] = "Hello";
const char *pstr = "Hello";
注意:"Hello"字符串常量为 const char* ,所以字符指针定义也要用const char*。(C语言中也可以只用 char*)
2、赋值:可以将字符串整体赋值给字符指针,而字符数组除了初始化的时候是不能整体赋值的,如
char str[];
const char *pstr;
str = "Hello"; //错误
pstr = "Hello"; //正确
理解:字符数组名是一个常量,不能被赋值,而字符指针是一个变量,存储字符串常量的地址,变量的值(存储的地址)是可以改变的。
3、访问字符元素:访问字符串的字符元素,通过下标或指针移位的方式即可,和普通数组一样。如
const char *p = "Hello";
p[]; //'e'
p[] = 'A'; //报错,不能修改字符串常量的元素
char a[]="Hello", *pa=a;
pa[] = 'A'; //可以修改
注:要理解字符指针指向的是字符数组还是字符串常量。
数组指针
1、数组指针:指向一个数组的指针变量(注意不是指向数组的首元素)称为数组指针(也称行指针)。例如
int a[];
int *p1=a; //指向数组首元素的指针,指向的类型是int
int (*p2)[]=&a; //数组指针,指针指向的类型是4个int组成的数组
p1+ --> a[]
p2+ --> a[] //注意这里+1是4个int的大小
注意:赋值要使用p2 = &a,如果用p2=a会报错,因为a的类型是 int *, 而&a的类型是int (*)[4],这是两种指针类型(虽然地址一样)。
2、访问数组元素:由于数组指针指向一个数组,那么*p就是数组本身,(*p)[i]表示数组的第i个元素,例如
int a[]={,,,};
int (*p)[]=&a;
(*p)[] -->
数组指针与二维数组
1、数组指针用于二维数组,指针指向以二维数组每一行数组,例如
int a[3][4];
//可看成3个int [4]的数组:a[0]、a[1]、a[2]
int (*p)[4]=a; //定义指向int [4]数组的指针变量
p --> a[0]
p+1 --> a[1]
注意:二维数组名a为int (*)[4]类型。
2、访问数组元素,如
p[0][1] --> a[0][1]
*(*(p+1)+2) --> a[1][2]
演示-用数组指针遍历输出二维数组{{1,2,3,4},{5,6,7},{8,9}}。
注意:p[i][j] -- > *(*(p+i) + j)
#include <stdio.h>
int main(){
int a[][]={{,,,},{,,},{,}};
int i,j,(*p)[]=a;
for(i=;i<;++i){
for(j=;j<;++j)
//printf("%d ",p[i][j]);
printf("%d ",*(*(p+i)+j));
printf("\n");
}
return ;
}
指针数组与二维数组
1、指针数组:指定义一个由指针组成的数组,即数组中的每个元素都是指针变量,例如
const char *p[];
//由p[0]..p[3]组成的数组,每个都是char*指针
2、指针数组用于二维数组,下标表示二维数组的行数,例如:
int a[][];
//可看成3个int [4]的数组:a[0]、a[1]、a[2]
int *p[]; //定义3个数组
p[] = a[]; //指针赋值
p[] = a[];
3、数组元素:由于p[i]的类型是int*数组,其元素可以用 +j ,或下标[j]来访问,如
p[0][1] --> a[0][1]
*(p[0]+1) --> a[0][1]
演示-用指针数组遍历输出二维数组{{1,2,3,4},{5,6,7},{8,9}}。
注:p[i][j] -- > *(p[i] + j)
#include <stdio.h>
int main(){
int a[][]={{,,,},{,,},{,}};
int i, j, *p[];
p[] = a[];
p[] = a[];
p[] = a[];
for(i=;i<;++i){
for(j=;j<;++j)
//printf("%d ",p[i][j]);
printf("%d ",*(p[i]+j));
printf("\n");
}
return ;
}
指针数组与字符串
指针数组适用于指向若干字符串(每个字符串不定长,用二维字符数组很浪费空间),会使字符串处理更加灵活,效率更高。例如
const char *p[3]={"Hello","new world","how are you"};
p[0][1] --> 'e'
*(p[0]+1) --> 'e'
注:每一个字符串指针都是以'\0'作为结尾。
指向指针的指针
1、指针变量*p存储的是一个地址,它自己也有地址,可以再用一个指针变量*prt指向p的地址,prt称之为指向指针的指针变量,简称二级指针,例如 int i, *p=&i, **prt=&p; 注意此时指针类型为“指向整型数据的指针变量”,不是整型数据。 prt = &i; //错误,指针类型不符
2、对指针数组*p[n]来说,数组名代表首地址,每一个元素都是指针型数据,因此它就是一个二级指针,也可以用一个二级指针进行赋值及运算,例如
int a[4]={1,3,5,7};
int *pa[4]={&a[0],&a[1],&a[2],&a[3]};
int **p=pa;
**p --> 1
**(p+1) --> 3
指针与结构体
struct 结构体类型 *变量名;
struct student stu, *p;
p = &stu;
2、成员的引用:访问成员有以下三种方式:
stu.name;
(*p).name;
p->name; //指针专用,指向运算符,优先级比单目运算符高
p->age++; //先获得age成员,再++
练习-根据姓名查询学分
#include <stdio.h>
#include <string.h>
struct students{
int no;
char *name;
int score;
};
struct students *find(struct students *s, int len, const char *name)
{
while(strcmp(s->name , name) != )
s+=;
return s;
}
int main()
{
struct students stu[] = {{, "张三", },
{, "李四", },
{, "张思", },
{, "李武", },
{, "张武", }};
struct students *a = find(&stu[], , "李武");
printf("输出信息为,姓名:%s,学号:%d,成绩:%d", (*a).name, (*a).no, (*a).score);
return ;
}
指针作为函数参数
函数参数是指针变量时,传递的是指针存储的地址,而变量做参数时传递的是具体的值(会产生形参的拷贝)。指针做参数时,地址(指针自己的值)不能被修改,但地址指向的值可以被修改。例如
void change(int *p){
*p = ;
}
int main(){
int a=;
change(&a); //传入指针,在函数里a的值被修改
}
数组作为函数参数
数组名可作为函数的实参和形参,传递的是数组的首地址,在函数被调用时,对形参指向的值的修改会使得实参数组发生变化。由于传递的是首地址,那么也可以用指针来表示形参或实参,效果相同。
1、形参实参都用数组名:
void f(int arr[],int len){...}
int main(){
int a[];
f(a, );
}
2、实参用数组名,形参用指针变量
void f(int *x,int len){...}
int main(){
int a[];
f(a, );
}
3、实参用指针变量,形参用数组名:
void f(int arr[],int len){...}
int main(){
int a[],*p=a;
f(p, );
}
4、实参形参都用指针变量:
void f(int *x,int len){...}
int main(){
int a[],*p=a;
f(p, );
}
演示-设计函数cat,将第二个参数的字符串被连接到第一个字符串后面。
#include <stdio.h>
void cat(char *p1, const char *p2){
while(*p1) *p1++; //到p1的'\0'位置,
while(*p2)
*p1++ = *p2++; //将p2复制给p1的'\0'后面位置
*p1 = '\0'; //最后加个'\0'
}
int main(){
char s1[]="Hello"; //长度要够
const char *s2 = " world!";
cat(s1,s2);
printf("%s\n",s1);
return ;
}
指针型函数
返回值类型为指针的函数称之为指针型函数,如
int *num(int x, int y);
类型 *函数名([形参列表]);
注意:返回指针类型意味着不需要生成拷贝,效率较高。
理解:*的优先级低于()的优先级,因此函数名先和后面的()结合,以上格式意味着首先是一个函数,然后返回值是一个指针。如果是以下格式:int (*num)(int x, int y)
则先将*和num结合,表示num是一个指针变量,指向一个函数(称为函数指针)。
函数指针
1、指向函数的指针变量称之为函数指针。例如
int (*func)(int x, int y);
类型 (*函数名)([形参列表]);
函数指针变量指向一个函数的地址,可以将同样原型的函数赋值给变量,例如
int max(int x,int y);
int (*p)(int x,int y);
p = max; //将函数赋值给指针变量
定义了函数指针变量以后,就可以用此变量来调用函数,也可以将之作为函数的参数(回调函数)。
2、调用函数:用函数指针变量调用max和min函数。
int max(int x, int y); //求最大值
int min(int x, int y); //求最小值
int (*p)(int x, int y);
p=max;
printf("%d\n",p(,)); //
p=min;
printf("%d\n",p(,)); //
3、回调函数:又称callback,是一种事件驱动的编程模式,将函数指针变量作为参数传递给另外一个函数,函数里面当某些事件满足时会调用此函数指针变量指向的函数,例如。
void print(int n){ //回调函数,打印信息
printf("回调函数调用:%d\n",n);
}
void loop(int max, int (*p)(int n)){
//遍历1..n,如果7的倍数就打印信息
int i;
for(i=;i<max;++i){
if(i%==) p(i);
}
}
理解:诸葛亮交给赵云三个锦囊,如果发生**事情,就打开第几个锦囊,这里的锦囊就是传递进去的函数指针变量。
4、自定义函数指针类型:可以使用typedef来自定义一种函数指针类型,例如
typedef (*MYFUNC)(int n); //自定义类型PRINT_FUNC
MYFUNC p = print; //将print函数赋值给变量p
p(); //调用函数指针变量
动态分配内存
1、用malloc函数可以在程序中动态分配内存空间,返回void*指针,这时需要用一个指针变量将首地址保存起来,以后可以使用,如果没有保存,则这块空间就丢失了(称之为内存泄漏),例如
int *p = (int*)malloc(sizeof(int));
*p = ;
p = (int*)malloc(sizeof(int));
//p指向新地址,原内存丢失(泄漏)
注意:需要#include <malloc.h>。
2、动态数组:可以用malloc函数来定义动态数组。例如
int *p = (int*)malloc(sizeof(int)*);
//分配10个元素的数组
p[] = ;
3、释放内存:free函数用来将动态分配的内存空间还给系统,之后系统可以继续使用回收的内存。例如
int *p = (int*)malloc(sizeof(int));
free(p);
p = NULL;
int *p2 = (int*)malloc(sizeof(int)*);
free(p2);
p2 = NULL;
注意:指针变量被释放以后,不能再赋值,因为它没有内存空间,申请内存后才能赋值或运算。为了防止继续赋值导致崩溃,建议将指针设置为NULL。
4、栈和堆:malloc分配的内存空间从“堆”上申请内存,从低到高,而自动分配的如int a,是在“栈”上申请的内存空间,从高到低。