C程序设计语言——指针
指针与指针变量
指针变量:
定义指针变量:类型名 *指针变量名
指针:
取地址运算符和取值运算符
如果需要获取某个变量的地址,可以使用取地址运算符(&)
例:char *pa = &a;
如果需要访问指针变量指向的数据,可以使用取值运算符(*)
printf("%c, %d", *pa, *pb)
数组的地址
数组的地址为数组第一个元素所属的地址,又称为基地址
指针如何指向数组
直接使用指针指向数组的第一个元素即可
1 | int a[] = {1,2,3,4,5}; |
指针对数组的操作
- 当指针指向数组时,可以对指针变量进行加减运算。意思是获取数组的前或后几位元素。
图上是直接给p赋予地址,所以*p就直接获取的数组的第一个元素的值,这里即a.
这里再附张图对刚才的言论进行进一步论证。这里直接对p进行像数组这样的取值操作就是因为,我们刚开始&a[0]实际上是直接给p这个指针直接指向a这个数组。
当然了也可以通过直接对地址进行加减来获取数组的第几个元素。
这里的*(p+4)的解释为,p的值为地址值,刚才也说过可以对地址进行加减以此来获取数组的第n位元素,这里实际上就是对地址进行加减了,p+4获取的是第5个元素。
大家有注意到么?p我这里直接赋值a,获得的就是a数组的地址,原因在于我们p这里定义时为*p,所以如果直接赋值数组a的话将直接赋予数组的基地址。
无论指针是什么类型,都符合p+n为p指针对象的第n个地址
在计算p+n时,n将根据p指向的对象的长度按比例缩放,而p指向的对象的长度则取决于p的声明。例如,如果int类型占4个字节的存储空间,那么在int类型的计算当中对应的n将按4的倍数进行计算。
函数的参数指针的传递
我们知道C语言是值传递,值传递意味着我们每次传递都是一个副本,若想让我们传递的参数的影响离开调用函数也能保持修改有效,必须传递指针。
传递时传递了指针地址值的副本,或者说是拷贝了指针而没有拷贝具体的值。
但是每次位移都不针对原地址进行修改(实际上,传递的是一个字符数组,由于是数组其内存空间是连续的,其内部值是可以修改的,非数组的指针其指向内容无法修改,但是地址值不变),而是在这个副本的基础上进行修改。
也可以看到上图,进行++操作时,char类型的指针的地址的值加上了char类型的长度1,也就是说实际上我们在对指针进行加减操作时,实际上就是对地址值进行位移操作,位移的距离为+(或-)的数值乘以类型的长度,即这里的s++实际上就是s=s+1*1;
所以下面的这个函数
为什么能得出字符串的长度,实际上就是原字符的副本与另一个副本之间进行地址间的相减,比如我要计算char *s = "Hello";
的长度,当s = “”时,即s++,s向右移了5位,此时的地址的值比刚传递过来的”Hello”的地址要多5 * 1 的值,则这个值减去初始的长度,则为5除以char的位数,即5 / 1;最终值就为5.
为了佐证这个观点,我又做了这个实验
s++之后,也就是其地址值加了4 * 1的地址值,那这个函数最终返回什么?答案是: 1.
即地址之间进行相应的加减之后得到的结果还得除以位数。
有效的指针运算
- 相同类型指针之间的赋值运算。
- 指针同整数之间的加法或减法运算。
- 指向相同数组中的两个元素的两个指针间的减法或比较运算。
- 将指针赋值为0或指针与0之间的比较运算。
其余的指针运算都是非法操作。
指针与数组的差别
1 | char amessage[] = "Hello World!"; |
以上是一个数组,和一个指针。
以上两者区别:
- 数组的值是可以修改,但是其始终指向同一个内存地址。
- 指针其初值指向一个字符串常量之后它可以被修改以指向其他地址,但如果试图进行修改字符串内容,结果是没有定义的。
指针数组
举个例子:String[] a = {"1", "2"};
上面的a是什么类型?
字符串数组,实际上指针数组也是这么个意思,指针数组存的元素都是指针。
指针数组的初始化
与普通数组的初始化一样例如:
1 | char *arrays[] = { |
指向函数的指针(函数指针)
函数作为参数时,不需要添加&来获取地址,与数组一样。
在参数列表时表示为:int (*func)(void *, void *)
调用:(*func)(arg1, arg2)
声明和调用的函数名的括号是不能少的
复杂的声明
例如:
1 | int *daytab[13] |
语法形式:
1 | dcl: optional *'s direct-dcl |
*带有的是dcl**。
结构
结构是一个或多个变量的集合,这些变量可能为多个类型,为了方便处理而把这些变量组织在一个名字下。
结构可以拷贝、赋值、传递给函数,函数也可以返回结构类型。
结构的声明
示例:
1 | struct point { |
跟声明int x, y, z
这样的声明类似,结构也是能这样声明的:struct {...} x, y, z
关键字struct
引入结构声明。结构声明由一对花括号内的一系列声明组成。
结构标记
struct后面的名字是可选的,称为结构标记。
结构标记用于为结构命名,在定义之后,结构标记就代表花括号内的声明,可以用它作为该声明的简写形式。
如果结构声明的后面不带变量表,则不需要为它分配内存空间,它仅仅描述了一个结构的模板或轮廓。但是,如果结构声明中带有标记,那么在以后定义结构实例时,便可以使用该标记定义。
例如:struct point pt;
定义了一个struct point类型的变量pt。结构的初始化可以在定义的后面使用初值表进行。初值表中每个成员对应的初值必须是常量表达式。
例如:
1 | struct point maxpoint = {320, 380}; |
引用结构的成员
结构名.成员
,这种用法跟面向对象当中的一个实例或类名.成员的调用一致。
结构的合法操作
结构的合法操作只有几种:作为一个整体复制和赋值,通过&运算符取地址,访问其成员。
结构之间不能进行比较。
结构指针
声明:struct point *pp;
上面声明的是一个指向struct point类型对象的指针。
结构指针更推荐通过->调用其成员属性,当然通过.也是可以的,例如struct rect r, *rp = &r;
它可以有以下四种方式调用成员属性
- r.pt1.x
- rp->pt1.x
- (r.pt1).x
- (rp->pt1).x
上面4种都是等价的。
C语言中优先级最高的4个符号
- “.”
- “->”
- “()”
- []
因此,例如以下例子:++p->len;
该表达式实际上是对p这个结构体的成员len+1;
结构数组
结构数组的声明有两种:
1
2
3struct key {
...
} keytab[KEYS];1
2
3
4stuct key {
..
};
struct key keytab[KEYS];
上面两种声明的意义都是声明一个结构类型key,并定义了该类型的结构数组keytab,同时分配存储空间。
其赋初值方式为:
最好是
将每一行(即每个结构)的初值都括号在花括号内
{“auto”, 0}
{“break”, 0}
{“case”, 0}
结构数组的长度
其长度在编译时已经完全确定。它等于数组项的长度乘以项数。keytab的长度/struct key的长度
sizeof
sizeof可以用来计算任意一对象的长度。
表达式:sizeof 对象
orsizeof(类型名)
该函数返回一个整数值,它等于指定对象或类型占用的存储空间字节数。(严格地说,sizeof的返回值是无符号整数值,其类型为size_t,该类型在头文件<stddef.h>中定义。)其中,对象可以是变量、数组或结构;类型可以是基本类型,也可以是派生类如结构类型或指针类型。
自引用结构
一个包含自身实例的结构是非法的。
1 | struct tnode{ |
1 | struct t{ |
类型定义(typedef)
typedef用来建立新的数据类型名。
例如:typedef int Length;
以上将Length定义为与int 具有同等意义的名字。
联合(union)
联合是可以(在不同时刻)保存不同类型和长度的对象的变量,编译器负责跟踪对象的长度和对齐要求。
联合提供了一种方式,以在单块存储区中管理不同类型的数据,而不需要再程序中嵌入任何同机器有关的信息。
使用联合的目的
一个变量可以合法地保存多种数据类型中任何一种类型的对象。
不像结构那样,当赋值一次之后该联合对象将会是那个赋值类型,读取的时候也是那个最近赋值的那个类型的值。也就是说*不像结构一样能赋值多个属性,而是看类型赋值了什么类型就只有那个类型的属性,但是可以重新赋值其他的类型的值,但是读取出的那个类型的属性将是最近赋值的对象的类型的那个属性。
例如:
1 | union u_tag{ |
这个联合对象u,假设我先赋值int a = 5;把这个a赋值给u,则u此时只有ival这个属性,u的长度也变为了4位。
联合的语法结构
其语法基于结构
1 | union u_tag{ |
联合只能用第一个属性进行初始化,比如上面的u,其只能用int进行初始化。
联合对象的属性访问
与结构一样
- 联合名.成员
- 联合指针->成员
c语言的文件读写
File *fopen(char *name, char *mode)
该函数声明于*<stdio.h>*,第一个参数是包含目标文件名的路径,第二个参数是访问模式,读(”r”),写(”w”), 追加(”a”)。设计到二进制文件则在这些模式的后面加上”b”即可。
如果文件不存在时,读文件会造成错误,而写和追加则会创建文件,倘若不存在的话。倘若没有权限,读文件也会报错。
getc(File *fp)与putc(int c, File *fp)
getc从文件中返回下一个字符,它需要知道文件指针,以确定对哪个文件进行操作; putc是一个输出函数,该函数将字符c写入到fp指向的文件中,并返回写入的字符。
int fclose(File *fp)
执行和fopen相反的操作,它断开由fopen函数建立的文件指针和外部名之间的连接,并释放文件指针以供其他文件使用。其还有一个作用是把缓冲区中由putc函数正在收集的输出写到文件中。
char *fgets(char *line, int maxline, File *fp)
fgets函数从fp指向的文件中读取下一个输入行(包括换行符),并将它存放在字符数line中,它最多可读取maxline-1个字符。读取的行将以’\0’结尾保存到数组中。通常返回line,但如果遇到文件结尾或发生错误,则返回NULL。
其在标准库里的具体实现
1 | char *fgets(char *s, int n, File *iop) |
可以看到fgets是基于库函数gets,该函数与fgets相似,但它们是对stdin和stdout进行操作;gets函数在读取字符串时将删除结尾的换行符(‘\n’)
int *fputs(char *line, File *fp)
输出函数fputs将一个字符串(不需要包含换行符)写入到一个文件中
如果发生错误,返回EOF,否则返回一个非负值。
其在标准库里的具体实现
1 | int fputs(char *s, File *iop) |
<string.h>库函数
strcat(s, t)
将t指向的字符串连接到s指向的字符串的末尾
strncat(s, t, n)
将t指向的字符串中前n个字符连接到s指向的字符串的末尾
strcmp(s, t)
根据s指向的字符串小于、等于、大于t指向的字符串的不同情况,分部返回负整数、0或正整数
大部分场景用来对比两个字符串是否等价
stcncmp(s, t, n)
同strcmp相同,但只在前n个字符中比较
strcpy(s, t)
将t指向的字符串复制到s指向的位置
strncpy(s, t, n)
将t指向的字符串中前n个字符复制到s指向的位置
strlen(s)
返回s指向的字符串的长度
strchr(s, c)
在s指向的字符串中查找c,若找到,则返回指向它第一次出现的位置的指针,否则返回NULL
strrchr(s, c)
在s指向的字符串中查找c,若找到,则返回指向它最后一次出现的位置的指针,否则返回NULL
存储管理函数
函数malloc和calloc用于动态地分配存储块。
void *malloc(size_t n)
当分成成功时,它返回一个指针,该指针指向n字节长度的未初始化的存储空间,否则返回NULL。
void *calloc(size_t n, size_t size)
当分配成功时,它返回一个指针,该指针指向的空闲空间足以容纳由n个指定长度的对象组成的数组,否则返回NULL。该存储空间被初始化为0.
free(p)
释放一个由malloc或calloc函数得到的指针所指向的存储空间。
随机数发生器函数
rand()
生成介于0和RAND_MAX之间的伪随机整数序列。RAND_MAX是在头文件<stdlib.h>中定义的符合常量。
生成一种大于等于0,但小于1的随机浮点数的方法#define frand() ((double) rand() / (RAND_MAX+1.0))
read和write低级I/O函数
输入与输出是通过read和write系统调用实现的。在C语言程序中,可以通过函数read和write访问这两个系统调用。
在使用这两个函数之前,记得#include <unistd.h>
read(int fd, char *buf, int n)
第一参数是文件描述符,第二个参数是程序中存放读或写的数据的字符数组,第三个参数是要传输的字节数。
write(int fd, char *buf, int n)
第一参数是文件描述符,第二个参数是程序中存放读或写的数据的字符数组,第三个参数是要传输的字节数。
以上两个函数返回实际传输的字节数。
在读文件时,函数的返回值可能会小于请求的字节数。如果返回值为0,则表示已到达文件的结尾;如果返回值为-1,则表示发生了某种错误。如果是在写文件时,返回值是实际写入的字节数。如果返回值和请求写入的字节数不相等,则说明发生了错误。
open、creat、close和unlink
open与fopen相似,不同的是,前者返回一个文件描述符,它仅仅只是一个int类型的数值,而后者返回一个文件指针,如果发生错误,open将返回-1
这三个函数使用前记得#include <fcntl.h>
int open(char *name, int flags, int perms)
第一个参数,包含文件名的字符串
第二个参数,是一个int类型,用它来说明以何种方式打开文件
其主要值有:
- O_RDONLY 以只读方式打开文件
- O_WRONLY 以只写方式打开文件
- O_RDWR 以读写方式打开文件
- O_APPEND 将写入追加到文件的尾端
- O_CREAT 若文件不存在,则创建它。使用该选项时,需要第三个参数mode,用来指定新文件的访问权限位
第三个参数perms用于给定权限,一般设置0即可,倘若设置了O_CREAT时,该参数需要设置创建文件之后该文件的访问权限,例如0755,当前文件的所有者可对其进行读写和执行操作,而所有者组和其他成员只能进行读和执行操作。
倘若打开一个不存在的文件,则将导致错误。可以使用creat系统调用创建新文件或覆盖已有旧文件
int creat(char *name, int perms)
如果成功创建文件,他将返回一个文件描述符,否则返回-1。如果此文件已存在,creat将把该文件的长度截断为0,从而丢弃原先已有的内容。perms用于指定的权限创建文件。
long lseek(int fd, long offset, int origin)
将文件描述符为fd的文件设置为offset,其中offset是相对orgin指定的位置而言的。随后进行读写的操作从此位置开始。origin的值可以为0、1、2,分别用于指定offset从文件开始、从当前位置或从文件结束处开始算起。
与此函数功能一样的是fseek只不过其第一个参数变为File *fd,且报错时返回一个非0值。