指针全称是指针变量,其实质是C语言的一种变量。这种变量比较特殊,通常它的值会被赋值为某个变量的地址值(p = &a),然后我们可以使用*p这样的方式去间接访问p所指向的那个变量。
指针存在的目的就是间接访问。有了指针之后,我们访问变量a不必只通过a这个变量名来访问。而可以通过p = &a; *p = xxx;这样的方式来间接访问变量a。
& //取地址符,将它加在某个变量前面,则组合后的符号代表这个变量的地址值。
//举例,将变量a的地址值赋值给p。
int a; int *p; p = &a;
a //代表变量a本身
p //代表指针变量p本身
&a //代表变量a的地址值
*p //代表指针变量p所指向的那个变量,也就是变量a
&p //代表指针变量p本身的地址值。符号合法,但对题目无意义
*a //把a看作一个指针,*a表示这个指针所指向的变量。这这里该符号不合法,因为之前已经定义a是个整形变量了。
* //指针符号。指针符号在指针定义和指针操作的时候,解析方法是不同的。
int *p; //定义指针变量p,这里的*p含义不是代表指针变量p所指向的那个变量,在定义时这里的*含义是告诉编译器p是一个指针。
int p; // p是一个整形变量
int *p; // p是一个指针变量,该指针指向一个整形数使用指针的时候,*p则代表指针变量p所指向的那个变量。
指针既然是一种变量,那么肯定也可以定义,也可以初始化。
//第一种:先定义再赋值
int *p; // 定义指针变量p
p = &a; // 给p赋值
//第二种:定义的同时初始化
int *p = &a; // 效果等同于上面的两句
指针变量本质上是一个变量,指针变量的类型属于指针类型。int *p;定义了一个指针类型的变量p,这个p所指向的那个变量是int型。
int *pInt; // pInt是指针变量,指向的变量是int类型
char *pChar; // pChar是指针类型,指向的变量是char类型
float *pFloat;
double *pDouble;
//注:各种指针类型和它们所指向的变量类型必须匹配,否则结果不可预知。
int *p;
//第一种:首先看到p,这个是变量名;其次,p前面有个*,说明这个变量p是一个指针变量;最后,*p前面有一个int,说明这个指针变量p所指向的是一个int型数据。
char *(*(*pfunc)[])(char *, char *) //类似的复杂表达式,可以用相同的分析方法得到
//第二种:首先看到p,这个是变量名;其次,看到p前面的int *,把int *作为一个整体来理解,int *是一种类型(复合类型),该类型表示一种指向int型数据的指针。
//总结:第二种方法便于理解,但是不够本质;建议用第一种方法来理解,因为这种思维过程可以帮我们理解更复杂的表达式。
数组名:做右值时,数组名表示数组的首元素首地址,因此可以直接赋值给指针。而且数组名是个常量,不能赋值,所以数组名不能做左值。
int a[5]; //则a和&a[0]都表示数组首元素a[0]的首地址。而&a则表示数组的首地址。
注意:数组首元素的首地址和数组的首地址是不同的。前者是数组元素的地址,而后者是数组整体的地址。两个东西的含义不同,但是数值上是相同的。
根据以上,我们知道可以用一个指针指向数组的第一个元素,这样就可以用间接访问的方式去逐个访问数组中各个元素。这样访问数组就有了两种方式:
int a[5]; int *p; p = a;
a[0] a[1] a[2] a[3] a[4] //数组的方式依次访问
*p *(p+1) *(p+2) *(p+3) *(p+4) //指针的方式依次访问
指针本身也是一种变量,因此也可以进行运算。但是因为指针变量本身存的是某个其他变量的地址值,因此该值进行*, / ,%等运算是无意义的。两个指针变量相加本身也无意义,相减有意义。指针变量+1,-1是有意义的。+1就代表指针所指向的格子向后挪一格,-1代表指针所指向的格子向前挪一格。
*p++; //就相当于*(p++),p先与++结合,然后p++整体再与*结合。
//*p++解析:++先跟p结合,但是因为++后置的时候,本身含义就是先运算后增加1(运算指的是p++整体与前面的*进行运算;增加1指的是p+1),所以实际上*p++符号整体对外表现的值是*p的值,运算完成后p再加1.所以*p++等同于:*p; p += 1;
*++p; //等同于 p += 1; *p;
(*p)++; //使用()强制将*与p结合,只能先计算*p,然后对*p整体的值++。
++(*p); //先*p取值,再前置++,该值+1后作为整个表达式的值。
总结:++符号和指针结合,总共有以上4种情况。--与++的情况很类似。
int add(int a, int b) 函数传参使用了int型数,本身是数值类型。实际调用该函数时,实参将自己拷贝一份,并将拷贝传递给形参进行运算。实参自己实际是不参与的。所以,在函数中,是没法改变实参本身的。
C语言函数调用时,一直都是传值调用。也就是说实际传递的一直都是实参的拷贝,但是在用指针传递时,传递的是实参的地址值,这样,让我们在函数中通过间接访问*p的方式,在函数内部访问到了函数外面调用时的实参。所以此时经过变换后,实参的值会发生改变。
没有结构体之前,在C语言中,数据的组织依靠:变量+数组。
最初最简单的时候,只需要使用基本数据类型(int char float double)来定义单个变量,需要几个变量就定义几个。后来情况变复杂了,有时需要很多意义相关的变量(例如需要存储及运算一个班级的学生分数)这时候数组出现了。数组解决了需要很多类型相同、意义相关的变量的问题。
但是数组是有限制的。数组最大的不足在于,一个数组只能存储很多个数据类型相同的变量。所以碰到需要封装几个类型不同的变量的时候,数组就无能为力。例如:使用一个数据结构来保存一个学生的所有信息:姓名 学号 性别。这时候就需要结构体。
struct Student
{
char name[20]; //学生姓名
unsigned int num; //学号
int isMale; //性别
}; //这里的分号不能少
//注意:1、结构体类型的定义是在函数外面,不是里面
//2、结构体定义的是一个新的组合类型,而不是变量,也不消耗内存,稍后在定义变量的地方,再使用该结构体类型来定义变量。
int main(void){
struct Student s1; //s1是一个变量,类型是struct Student
//给结构体变量复制
s1.name[0] = 'J';
s1.name[1] = 'i';
s1.name[2] = 'm';
s1.name[3] = '\0';
s1.num = 123;
s1.isMale = 1;
}
结构体是一个集合,集合中包含很多个元素,这些元素的数据类型可以相同,也可以不相同。所以结构体是一种数据封装的方法。结构体存在的意义就在于,把很多数据类型不相同的变量封装在一起,组成一个大的新的数据类型。
数据结构:把庞大复杂的数据用一定的方式组织管理起来,便于操作(查找,增加,删除等)这就叫数据结构。
新增关键字:struct
新增操作符:.,用来连接结构体和它里面的子元素
数组是一种特殊的结构体,特殊之处在于封装内的各个元素类型是相同的。结构体和数组都是对一些子元素的封装,因此定义的时候都是封装作为整体定义,但是使用的时候,都是使用封装中的子元素。一般结构体变量和数组变量都不会作为一个整体操作。
第一步:定义结构体类型。结构体类型的定义是在函数外面(函数外面 == 全局)的。
第二步:使用第一步定义的类型来定义结构体变量。
第三步:使用变量。实际上使用结构体变量的时候,使用的是结构体变量中封装的各个子元素,而不是结构体变量本身。
结构体变量和普通变量一样,作为局部变量时,如果定义的时候无初始化也无显式赋值,则结构体变量中的子元素的值是随机的。
//定义结构体
struct MyStruct
{
int a;
cahr c;
float f;
double d;
};
//结构体初始化
struct MyStruct s = {100, 'd', 12.445, 111.111111111}; //ok
struct MyStruct s =
{
//.a = 1444,
.c = 'k',
.f = 3.13,
.d = 32.333333,
}; //ok,可以部分初始化
//这种部分ok的是最危险的,千万别用
struct MyStruct s =
{
s.a = 1444, //只能从前到后一次初始化,中间只要空一个,后面的就会出错,编译还不会报错
s.c = 'k',
s.f = 3.13,
//s.d = 32.333333,
};
第一种,完全初始化。{xx, xx, xx, xx, xx};
第二种,部分初始化。
{
.a = xx,
.b = xx,
.c = xx,
.d = xx,
};
共用体union在定义和使用形式上,和结构体struct很相似。但是两种数据结构是完全不同的两类东西。
//定义共用体
union MyUnion
{
int a;
char c;
float f;
};
int main()
{
union MyUnion u1; //使用自定义的union类型来定义变量
u1.a = 123;
printf("u1.a = %d, u1.c = %d", u1.a, u1.c); //u1.a = 123, u1.c = 123
return 0;
}
结构体,是对多个数据的组合与封装。
共用体,共用体中只有一个东西,只是它被好几个名字(和类型)共用。
在C语言中,一般使用常数的时候,都不是直接使用,而是先把该常数定义为一个宏,然后在程序中使用该宏名。这样做的好处是,等我们需要修改这个常数时,只需要在宏定义处修改一次即可。而不用到代码中到处去寻找,看哪里都用过该常数。
#define N (321) //宏定义的格式
#define SEDC_PER_YEAR (365 * 24 * 60 * 60)UL //使用宏定义定义一年中的秒数,UL是将(365 * 24 * 60 * 60)强制转换成一个无符号长整数
1、宏定义一般是在函数的外面。
2、宏定义必须要先定义,再使用宏。如果先使用就会编译报错。
3、宏定义中宏名一般用大写。不是语法规定的,是一般约定俗成的。
枚举是列举的意思,枚举是宏定义的一种替代,一种优化。
//定义一个枚举类型,名字叫rnum week
enum week{
SUN, //SUN = 0
MON, //MON = 1
TUE, //TUE = 2
WEN, //WEN = 3
THU, //THU = 4
FRI, //FRI = 5
SAT, //SAT = 6
};
int main(void){
enum week today; //使用enum week类型,来定义一个枚举变量today
today = SAT;
switch(today)
{
case MON:
printf("好困!\n");
break;
...
}
}
注意:
1、不能有重名的枚举类型。即在一个文件中不能有两个或两个以上的enum被typedof成相同的别名。
2、不能有重名的枚举成员。