(一)指针
了解指针首先要理解地址,我们是通过”&”运算符来获得一个变量的地址,地址可以通过%p进行输出,尽量不要用%x的十六进制输出,以后就这样来看变量的地址。
取地址符的操作数只能是变量,不可以有&(i++)这样的写法。
尝试了对于普通变量进行取地址,我们尝试对数组来取地址:
int a[]={1,2,3};
printf("%p\n",a);
printf("%p\n",&a);
printf("%p\n",&a[0]);
printf("%p\n",&a[1]);
控制台输出:
000000000062FE40
000000000062FE40
000000000062FE40
000000000062FE44
可见对于数组的取地址运算,可以不带取地址符,并且对于数组取的地址就是数组第一个元素所在的地址。
我们知道了变量是有地址的,而且通过取地址符我们可以获得变量的地址,那么我们遇到的问题就是两个:
1.取到的数组应该放到什么变量里 2.如何直接访问地址里的内容
第一个问题:我们是通过指针这种变量来保存地址的。
定义一个指针来存储地址的语法是:
Int*p=&i;
也就是定义了一个指针p来存储i的地址,我们也叫做指针p指向i,如果希望同时定义两个指向int型变量指针变量,请用:
int*p,*q;
C语言当中没有int*这种数据类型,诸如int*p,q,其实是定义一个指向int变量的指针变量p和int型变量q,
当指针做参数时,做形参基本的格式是 void f(int *p); 作实参时:f(&i) ,这样我们就在函数里得到了外部i的地址,存在指针p里,这样函数就可以访问外部的i了,于是我们有*运算符: *p表示访问指针p所指向的对象,此时*p可以作左值,也可以作右值,也就是说我们既可以读也可以改这个参数:
void fun(int *p){
*p=66;
}
int main(){
int t=22;
int *p=&t;
printf("%d\n",t);
fun(p);
printf("%d\n",t);
return 0;
}
控制台输出:
22
66
虽然此时仍然是遵循函数值传递的基本规则,但是借助传指针我们实现了函数访问函数外的变量。
我们知道了传普通变量和传指针都是值传递,那么传数组是什么情况呢?函数形参的数组变量接收到了什么呢?我们来看看int []a不作形参和作形参时sizeof(a)的具体表现说起:
对于形参的数组名取sizeof()我们会得到当前系统字长数/8,而在函数外则能得到数组的长度,原因在于实际数组传入函数内时,函数名已经变成一个普通指针了,换句话说sizeof((a[]),q其实就是sizeof(int *p),所以得到的是指针所占的字节数。
那么数组变量是什么呢?数组变量是一种指针常量:int* const p,所以不能通过a=b的方式用一个数组对于另一个数组进行赋值,而可以定义一个指针变量,用数组名去初始化,指针所支持的*可以用于数组,数组支持的[]可以用于指针。
那什么是指针常量呢?一个地址涉及到了两个变量,也就是自己所在的指针变量,以及 自己指向的那个变量,这两个变量都可以被限制成常量,若是指针被限制不能修改,那么指针就不能指向其他地方,p++这种事情绝不能发生,我们称之为指针常量,若是指针指向的内容不能修改,我们称之为常量指针。
写法有:
int* const p;
const int*p;
int const*p;
三种,区分在于const后面跟着什么,什么就被限制了,看看它是什么变量,若是指针p,那就是指针变量p被限定了,所以就变成了指针常量,不是p而是*p那就是常量指针。
常量指针是在形参当中出现的,表示传入的内容不可以被修改,比如:void f(const int* p);
调用时:
int k=7;
int* q=&k;
f(q);
q本身是一个指针变量,形式结合的时候初始化p这个常量指针,常量指针限定了指针所指向的内容不可以变化,所以*p不可以变化,也就是k不会发生变化。
当然对于本例子,其实直接f(k),就能达到效果了,但是主要是用在我们需要将大体积的对象传入函数的时候,形实结合就要复制该对象,会比较耗费资源,若传对象的指针就很方便,但是有时我们只想用对象的值而不想它被改变,利用常量指针就可以解决这个问题,如果就是希望被改变那是不需要加const的。
我们知道数组本身已经是指针常量了,但是我们也可以在数组名前加const,此时 const的意思是数组当中每一个元素都是const,相当于一堆变量都是const,我们知道常量是要定义时就初始化的,所以常数组在定义时也也马上初始化。若将这样的数组作形参,也就是数组的每一个元素都不能被修改,这样就能保护数组了。
指针类型作为一种变量类型,也具有与变量类型相关的各种运算:
(1)算术运算:有+,-,++,---,表示指针前进或者后退若干个元素,具体地址变化与指针所指向的数据的类型有关,减号表示相距几个元素,*p++可以实现对于特定尾巴数组的遍历:
int a[]={1,2,3,4,5,6,-1};
int *p=a;
while(*p!=-1){
printf("%d ",*p++);;
}
(2)赋值运算:每一个进程都有自己的零地址,用来完成一些特殊的任务,零地址不能写入内容,C语言当中预定义了一个零地址符号NULL,为了防止刚定义的指针里面残存的数据直接使用导致指针乱指,我们应该将它指向零地址,保证其安全无害,不建议使用字面值0;
(3)关系运算:由于内存地址是线性的,因此关系运算对于指针仍然适用。
(4)类型转换 可以使用(void *)将其转换为void型的指针,再转成其他类型的指针
指针的用途:
1.传入较大的数据时作参数
2. 传入数组后,对数组进行操作
3. 函数返回多个结果,指针带出结果
4. 动态内存分配
动态内存分配:
为了在程序运行过程时确定数组的大小,需要动态分配内存,是通过malloc函数实现的:
malloc函数:
需要:<stdlib.h>头文件
函数原型: void* malloc(size_t count) //参数是一个整数,返回一个void*类型的地址,根据需求转换
一般的用法: int*p=(int*)malloc(num*sizeof(int)); 配合free(p);
注意事项:free的时候不能p++以后free,free的位置就不对,不能free非动态内存分配而来的内存,不能free以后再free
(二)结构体
结构体:有些数据实际上需要多个类型的数据成员组成,于是引入了结构体,使用结构体本质是我们先声明一个类型,再定义该类型的变量,再对该变量进行赋值,作为变量也可以传参等等。
定义一个结构体类型:
第一个问题:我们应该把结构体声明在哪里?
我们推荐把它单独拿到所有函数外面,放在所有函数之前,这样所有函数都能使用该结构体类型
第二个问题:如何声明一个结构体:
(1) 先声明一个结构体,再定义结构体变量:
(2) 声明一个无名结构体,并直接定义所有我们需要全部的结构体变量
(3) 声明一个结构体并直接定义结构体变量:
//方法一:
struct date{
int month;
int day;
int year;
};
......
struct date today;//这一句在函数里,struct不可省略
//方法二:
struct date{
int month;
int day;
int year;
}today; //声明了结构体马上拿到变量
//方法三:
struct {
int month;
int day;
int year;
}today,yesterday; //一共就需要两个,全部拿了,结构体类型以后也不用了
第三个问题:如何初始化一个结构体变量?
作为一个变量,你不初始化它,它里面就是垃圾数据,结构体的本质与数组非常接近,数组是同类型元素在内存里依次紧密排列,结构体是不同类型的数据成员在内存里紧密排列,因此二者十分相近,我们可以模仿数组初始化的方法初始化结构体变量:
struct date yesterday={11,15,1994};
struct date tomorrow={.day=22,tomorrow.year=1996}; //出现了.运算符,用于访问成员
printf("month=%d day=%d year=%d\n",yesterday.month,yesterday.day,yesterday.year);
printf("month=%d day=%d year=%d\n",tomorrow.month,tomorrow.day,tomorrow.year);
控制台显示:
month=11 day=15 year=1994
month=0 day=22 year=1996 //可见与稀疏矩阵的赋值一样,未赋值的项会置为0
第四个问题:作为一个变量,结构体可以参与那些运算
不同于数组,结构体不是常量,因此可以参与赋值运算。例如:
struct date yesterday;
struct date tomorrow={.day=22,tomorrow.year=1996};
yesterday=tomorrow; //可以结构体变量之间直接赋值
tomorrow=(struct date){10,1,1949}; //可以将一组数转换为一个结构体变量
printf("month=%d day=%d year=%d\n",yesterday.month,yesterday.day,yesterday.year);
printf("month=%d day=%d year=%d\n",tomorrow.month,tomorrow.day,tomorrow.year);
对于结构体也可以取指针。它的名字并像数组一样是常量指针了,只是简单的变量名。对于结构体取的指针p,可以用传统的(*p).member来实现对于数据成员的访问,但是C语言有另一种运算->可以访问p所指向结构体的成员。
在函数当中使用结构体做形参,实际发生的是形式结合,函数体内形成了一个值相同但是与外界无关的结构体,消耗较大,我们通常传入一个常量结构体指针,达到传入结构体的目标,函数也可以返回结构体。
第五个问题:结构和数组都是承载基本类型的容器,他们可不可以互相容纳?
是可以的,我们可以在数组当中放数组,形成二维数组,也可以在结构里放结构,当然也可以混合,结构的成员是数组,或者数组的成员是结构。
以一个长方形数组为例,每个元素是长方形,长方形结构的两个数组成员是两个点,点结构的两个数据成员是x和y两个坐标:
#include <stdio.h>
typedef struct point{
int x;
int y;
}Point; //定义了一个Point结构体
typedef struct rectangle{
Point p1;
Point p2;
}Rectangle; //Rectangle结构体里包含了Point结构体作为其成员
Rectangle* fuzhi(Rectangle *p){
scanf("%d",&p->p1.x);
scanf("%d",&p->p1.y);
scanf("%d",&p->p2.x);
scanf("%d",&p->p2.y);
return p;
} //
void show(const Rectangle*p){
printf("p1.x=%d \n",p->p1.x);
printf("p1.y=%d \n",p->p1.y);
printf("p2.x=%d \n",p->p2.x);
printf("p2.y=%d \n",p->p2.y);
}
int main(){
Rectangle*p;
Rectangle m[3]={
{{1,2},{3,4}}, //展现其数组的性质
{{5,6},{7,8}}, // 我只对两个元素进行赋值 第三个元素像数组一样,值全部置0
};
for(int i=0;i<sizeof(m)/sizeof(m[0]);i++){
printf("第%d个长方形:\n",i);
show(&m[i]);
}
printf("------------\n");
fuzhi(&m[sizeof(m)/sizeof(m[0])-1]); //调用fuzhi函数对于第三个元素进行赋值
for(int i=0;i<sizeof(m)/sizeof(m[0]);i++){
printf("第%d个长方形:\n",i);
show(&m[i]);
}
return 0;
}
控制台输出:
第0个长方形:
p1.x=1
p1.y=2
p2.x=3
p2.y=4
第1个长方形:
p1.x=5
p1.y=6
p2.x=7
p2.y=8
第2个长方形:
p1.x=0
p1.y=0
p2.x=0
p2.y=0
------------
2 3 5 9
第0个长方形:
p1.x=1
p1.y=2
p2.x=3
p2.y=4
第1个长方形:
p1.x=5
p1.y=6
p2.x=7
p2.y=8
第2个长方形:
p1.x=2
p1.y=3
p2.x=5
p2.y=9