C语言基础
变量存储
全局变量与局部变量
全局变量的特点:
-
程序块之外的算全局变量(常常放在 main 函数之前),但是有先后识别顺序。
-
全局变量适合给函数直接传参,但是程序运行的时候都占内存,而且每一部分具有依赖性,迁移性差。
-
大量的全局变量使得牵扯太多,可读性差。
extern 标识符:
- 标识着这个变量或者函数要在其他的文件中寻找,即在别处定义,此处需要引用,在 gcc 编译中跨文件变量的使用很有用。
- extern 外部变量声明(要区分定义和声明),忽视读取先后顺序。
1 | extern int a,b; |
-
extern 在局部中声明,只对这一部分的代码块有效。
-
如果和代码块的局部变量重名,那么这个代码块不影响这个全局变量,按照里面的局部变量处理。
-
全局变量跨文件引用,默认是不支持的,但是可以在本文件里声明了 extern 后,就不会分配内存,可以引用了。(注意,这样交叉使用,可读性会很差)
变量的存储和使用
静态存储方式和动态存储方式
静态存储方式:分配固定存储空间。
全局变量放在静态存储区,程序结束后才释放。
动态存储方式:根据需要动态发分配存储空间。
形式参数、局部变量和函数的返回值以及被调用时的值存储在动态存储区。
static
设置局部静态变量,一般而言函数或者程序块调用完了,内存就会销毁。但是设置 static 声明局部静态变量,保存原来的内存,下次继续使用,而且只在编译时赋值一次。如果局部静态变量不赋初值,那么默认为 0。
如果用在全局变量之前,表示只能在本文件中调用
1 |
|
注意它仍然是局部变量,其他的程序块不能引用。而且,声明了之后 static 就不能跨文件引用了
外部函数和内部函数
标准是能否被其他源文件调用。
内部函数:只能在本文件里使用。只需要在函数前面加上 static,因此也被称为静态函数。
外部函数:不用 static 就是外部函数。默认支持函数跨文件引用。也有在前面加 extern 来声明的,但是着不必要。只需要在前面加函数声明即可。
预编译
宏
不带参数的宏定义
定义的格式:#define 宏的名字 字符串
终止的格式:#undef 宏名
好处:用简单的名字代替复杂的字符串,在修改的时候非常好用,也提高了可移植性。
//在编译的时候替换的过程,就叫做宏展开。
1 |
说明:
- 宏名一般用大写字母,这是习惯
- 不是 C 语句,不要加分号,否则分号会被一起替换
- 不能跨文件使用。
- 宏定义中可以使用其他宏。
- 双引号(字符串)内的宏名不替换,
带参数的宏定义
格式:#define 宏的名字(参数表) 字符串,#define S(a,b) a*b
-
只是替换,不会做其他处理。
S(1+5,2+3)
表示1+5*2+3
,计算结果不是原来的求积
所以常常加括号处理,#define S(a,b) (a)*(b)
-
复杂语句中有时候使用多行语句进行宏代替
1
2
3 -
宏展开是在编译时进行的,展开时不分配内存,也没有返回值这种说法,也没有值传递的说法。
-
宏替换只占用编译时间,不占用运行时间。而函数调用占的是运行时间(分配内存,传递参数,执行函数体,返回值) ;
文件包含
本质上:将另外文件中的代码包含在这个文件中
格式:#include “文件名” 其中文件名常常为 .h 文件,叫做头文件
- 一个 include 只能包含一个文件。
- 可以嵌套包含。一个头文件中,还能包含其他头文件。
- 方便使用现成的代码,也方便公共修改。
- <>表示到系统目录找文件,“ ”表示首先在当前目录查找,找不到再到系统目录找。
条件编译
格式:
方式一:
1 |
|
方式二:
1 | #ifndef 标识符 //如果没有定义过这个标识符,则执行这一段。 |
//方式三
1 | #if 表达式 //如果表达式为真,则执行这一段。 |
优点:
- 编译的时候只剩下满足条件的那一块,可以减少文件长度。
- 跨平台时,条件编译时预设好不同平台需要编译的代码。
- 方便测试,只需要删除宏定义就可以不执行特定代码块。
指针
指针非常重要!指针式 C 语言的重要特点,效率非常高,但是非常灵活。
前提知识
认识
- 变量可能保存在不同的存储区,比如静态存储区、动态存储区等。
- 变量分配内存的时间不同,有些事编译的时候分配内存,有些则是运行时分配。
- 每种数据类型都会占用内存。例如 int, char, float 等,可以用
sizeof()
查看。
地址的概念
计算机中用十六进制的数来表示地址,0x 开头,表示十六进制。
**严格区分地址和地址代表的内容。**一个地址代表一个字节,但是我们以第一个占用的地址为这个变量对应的地址。程序内部维持一张表,记录着地址和变量名的对应关系。
直接访问和间接访问
直接访问:按地址取址,直接从地址中存取变量。
间接访问:将变量的地址存放在另一个内存单元中,即一种特殊变量来存储地址。例如:p=&a,&是地址符号。这就是 p 的地址指向了 a 的地址,而 P 的地址一般使用四个字节的内存,里面储存着 a 的地址。
间接访问的读取的过程:
- 先找到存放 p 的地址,然后从 p 的地址中取出 a 的地址。
- 然后从 a 的地址中找到存储的内容。
- 通过映射表来把存储的内容映射成 a.
我们就说像上面 p 那样,专门存放另外一个变量的地址的变量,就叫做指针变量。指针(地址)的值就是指向的变量的地址。
指针变量
格式:类型标识符号 *指针变量名
1 | int i,j; |
- 定义指针变量的时候要加
*
,但是使用的时候不加*
,指针变量名是不含*
的。 - 指针变量只能指向同一类型的变量。
- 指针变量只存地址,不要乱赋值。
运算符:&:取地址运算符。
*:间接访问运算符。
如果不是定义指针变量或者当作乘法运算,*指针变量名 表示所指向的变量
注意事项:*p=a
- &*p。因为指针运算符和乘法运算符的优先级相同,而且是从右至左结合。所以相当于&(*p),即 p 指向的变量的地址,相当于 p。
- *&a。&a 表示 a 的地址,也就是 p 的值,而*p 即指向的变量,相当于 a。
- (*p)++。相当于 a++。
- *p++。因为++和*的优先级相同,而且是从右到左运算,所以等价于*(p++),而指针变量加一,代表跳过存储这一块内存占用的地址,到下一块内存。比如 int 类型占用 4 个字节,那个 p++的值加 4.但是,在引用的时候,*p++ 是先用后加,返回值是*p,然后 p++
- *++p。先加后用,返回值是*(p+1).
指针赋值代表指向传递
数组的指针和指向数组的指针
特点:
-
数组里面的元素的地址是相连的。
-
数组的地址是第一个元素的地址。
-
数组名就代表数组地址。
定义指针变量并赋初值:
1 | int *p=&a[0]; |
通过指针引用数组元素:
1 | //第一类,赋值 |
a[i],p[i],*(P+i),*(a+i),都代表引用数组元素。
1 |
|
实际上,系统中是根据 a[i],推算*(a+i),所以通过指针来引用,效率是比较高的。
注意
- 数组首地址不能更改。a++不合法。
- 不要动自己未定义的内存。
数组作为函数参数
- 实参形参都是数组名,那么函数就可以改变实参数组的值
1 |
|
其中,a,ba 公用同一段内存,指的是同一个数组。
- 实参是数组名,形参是指针。
这时赋值时和两者都是数组名,几乎一样。
- 实参和形参都是指针变量
这时赋值时和两者都是数组名,几乎一样。
- 实参为指针,形参为数组名
1 |
|
这几种方法很类似,指针数组作为参数传入,也会被当作数组。
多维数组:
多维数组在内存中是连续存放的,注意顺序,a[3][3][4],从右边开始变,且递增。下边这样变化,000,001,002,003,010 …213,213,220,221,222,223。
注意:
- a+i,指的是 a[i]行的内容。
- a[0],代表的是地址,即 a[0]=&a[0][0].
- a[0]+1,代表在 a[0]这一行,a[0][1]的地址,即 a[0]+1=&a[0][1].
- *a=a=a[0],所以 a[0]+1=*a +1.
- *(a+1)+2,指第一行第二列的地址。
核心规律:
二维数组的地址是地址指向地址,一层一层嵌套,*表示进入当前这一层嵌套,a[]也表示进入了这一层的嵌套。
指针数组:
1 |
|
数组指针:
1 |
|
1 |
|
字符串的指针
字符串的表现形式
1 |
|
字符数组的指针规律不变
因为字符数组实际上是转码后拷贝到str1[]
,分配不同的内存,和数字的数组是一致的。
常量的字符指针
因为内存中有一段类似字符数组的东西存放字符串常量,所以 指向同一个字符串常量的指针是相同的
这样很方便灵活的修改。
1 |
|
字符指针做函数参数
1 |
|
区分字符指针与字符数组
- 字符数组是由若干个元素组成,每个元素中存放一个字符;字符指针存放的事字符串的首地址,不存放内容。
- **赋值方式的差别:**初始化字符数组时,是把常量拷贝给字符数组。字符指针初始化时,是把指针指向常量的地址。
- 字符数组的地址不可以更改,但是字符指针可以再次定义指针的指向。
函数指针
函数指针的用法
当编译的时候,函数就会分到一个地址,那么通过调用这个地址,就可以调用这个函数。
**格式:**数据类型标识符 (*指针名)(形参列表)
1 |
|
注意:
*p
两边的括号不能省略,括号优先级比指针高。- 函数名代表函数的入口地址,
p = 函数名
表示p
也指向函数的开头,p=&max
也可以 p
不能指向语句,所以p+1
是不合法的。- 函数调用的时候,
*p
可以写成p
,即p(14,9)
- 在某些编译器中函数的入口地址和实际地址不一样,编译器会把入口地址映射到其他地址,在使用的时候使用实际地址,所以可能出现函数地址和指向函数的指针不一样的情况。
函数指针作为函数参数
指向函数的指针也可以作为另一个函数的参数。
1 |
|
返回指针值的函数
格式 数据类型 *函数名(参数列表),如 *p(int x, int y)
,因为括号的优先级比指针符号高,所以相当于
*(p(int x, int y))
,它指向函数的首地址,里面既有可引用执行的函数也有函数的返回值,。
注意不要和函数指针弄混了。
注意,绝对不可以读写被回收的地址,这种错误在引用函数中的局部变量时特别容易出现
指针数组、多重指针、main 函数参数
写法与区分
指针数组中的每一个元素都是指针类型的数据,都是指针变量。例如int *p[4]
,注意区分int (*p)[4]
,前者的每个元素都是指针,而后者的每个元素都是整型,方便存储地址和引用这个地址(因为(*P)[i+1]
和*(p+1)
,是等效的。
用途:多个字符串
因为每个常量都是有固定的地址的,我们就用指针数组来指向这个地址,实现存储多个字符串。
1 | int main() { |
指向指针的指针
int **p
和char **P
因为指针符号是从右到左结合,所以相当于int *(*p)
,表示p
是指向一个指针变量,*p
是p
指向的指针变量。
int a = 5;
int* p =&a;
int ** pp=&p;
printf("%d %d %d", a, *p, **pp);
注意这里int* p =&a;
,实际上是int* p;p =&a;
,也就是p
接收到的是地址。
用途
用于设置多维数组比较方便,尤其是可变长度的数组
1 | void move(char b[],int row,int col) |
在传参的时候,将多维数组转化成一维数组,然后再这样把一维数组转化成多维数组。
指针做 main 函数的形参
我么使用 main 函数时常常是把他的参数为空,如果传入参数
1 | in main(int argc, char *argv[]) |
那么默认argc
等于 1,argv[0]
默认是可执行文件的绝对路径。
另外如果再编译器中设置或者是是在命令行中设置
右击项目名,属性,配置属性,调试,命令参数。在这里可以加入字符串,注意用空格区分不同的字符串。
这时argc
的个数会增加,argv
会多存储这几个字符串。
也可以在 shell 里执行文件时 添加参数
回顾
指针变量的数据类型区分
指针变量运算
指针变量的加减是跳到相连的存储单元。
指针变量赋值不能够自己赋值给指针变量
空指针默认是 NULL,就是整数 0,而系统不会在地址为 0 处存在有效数据。默认赋空值是个好习惯。
万能指针
void*p
表示万能指针变量,可以指向任何类型的指针
结构体和共用体
为了将多种数据类型整合起来,我们引入结构体。结构体实际上是一种类型。
定义结构体类型变量
格式:struct 结构体名{成员表列};
1 | struct student |
**方法一:**把结构体当成数据类型,常规定义变量
struct student s1,s2;
**方法二:**定义结构体的同时,定义变量;
1 | struct student |
**方法三:**直接定义结构体类型的变量,定义时省略结构体名,但是后续不能够再定义这样的的结构体类型的变量了。
1 | struct |
注意:
-
一般是使用方法一,分两步,先定义结构体,再定义结构体变量
-
结构体类型可以嵌套
1 | struct student |
- 结构体内的变量名不影响程序中的变量名
引用结构体类型变量
-
不能将结构体变量作为整体引用,只能对结构体中的成员分别引用。
例如:
s1.num=1;
表示将 1 赋值给s1
变量中的成员num
,注意结构体成员运算优先级非常高,和括号是平级的. -
如果成员本身又属于结构体类型,就需要一步一步的找到最低级的成员。例如
s1.birthday.day
-
成员变量当成普通变量,成员变量也是有地址的,各种运算都是一致的。例如
s1.num++
,但是s1++
是不允许的。
结构体变量的初始化
定义时按顺序赋值,例如s1={19,Bob,1,18,"main street 503",4,5,2002}
如果时数组的话就像一般的数组一样来初始化。
1 | struct student stu[3]={ |
数组的引用和一般的数组也是一致的。
结构体的指针
指针的类型由结构体确定 struct student *p
给成员赋值
**方法一:**指针得到地址,然后用指针符号和指针名代替变量
1 |
|
**方法二:**使用->
,指向结构体成员运算符。实际上我们不用(*p).age = 20;
的方式来用指针赋值,而是p->age=20
,注意 arrow operator 的优先级非常高,也可以整体看成一个变量。
指向数组
1 | int main() { |
指向结构体的指针作为函数参数
因为时指向指针的,因此直接改变内存中的值,不过,如果是传入结构体,就不会改变原来地址中的值。
共用体
把几种不同类型的变量存放在同一段内存单元,每次赋值都会清空原来的内容,赋值上新的内容。因此每个时刻只有一个成员起作用。
形式:union 共用体名 {成员列表}变量列表;
和结构体的形式除了标识几乎一样。但是共用体占的内存是最长的成员的内存,而结构体占的内存是所有成员的和。
注意:
- 共用体的地址和每个成员的地址相同。
- 共用体不能在定义时初始化。
- 不能把共用体变量作为函数参数。
枚举类型
用枚举类型可以使分类分别用整型标识,这样非常明确类型的含义。实际上枚举这个命名并不准确,更准确的是一一对应的常量。
定义类型:
1 | enum color |
定义变量:enum color c1,c2;
-
可以直接定义枚举变量
-
red,green,blue,yellow
叫枚举常量,当作整型,值不可改变,默认从 0 开始,也就是分别对应0,1,2,3
-
枚举型变量可以用枚举型常量赋值
c1=red;
-
定义枚举常量时,可以特定的赋值,后面的自动加一。
1
2
3
4enum color
{
red=-8,green,blue=10,yellow
}; -
可以强制用数字赋值
c1=(enum color)1;
-
枚举常量可以当作整型做任何整型可以做的操作。
定义类型别名
typedef 原来的类型名 别名
,
- 别名一般用大写字母
- 编译时处理
- 用于定义类型名,而不是定义变量。
- 作用主要是在程序的通用性和可移植性,例如需要改类型,那么就只要改
typedef
这一行。
简单定义
typedef int INEGER
,这样就给 int
设置了一个别名
定义结构体的别名
,例如下面定义坐标
1 | typedef struct point |
定义数组别名
定义元素为 100 的整型数组typedef in NUM[100]
,使用时NUM n;
,那么就相当于int n[100]
定义字符指针
typedef char *PSTING
,使用时PSRING p,q;
,就相当于char* p,q;
实际上,先写常规的类型名字,然后把变量名替换成别名,在前面加上typedef
,然后就可以用这个别名代表这个类型。
位
介绍
一个字节由 8 个二进制位,最左边的称为二进制位,最右边的称为最低位。
位运算是针对二进制数字而言的
-
&
:相同的位置,只有都为 1 才为 1。 -
|
:相同位置,只要有一个为 1,就为 1 -
^
:(亦或)相同位置相同才为 1 -
~
:单目运算符。按位取反,0 变成 1,1 变成 0. -
<<
:将二进制位左移若干位,右侧补 0。反映在十进制上,是数 ×2 -
>>
:右移。超过的最低位会被舍弃,相当于 ÷2.&=,|=,^=,~=
也可以作为符合运算符使用,和+=,-=
类型
应用
主要是在密码学和算法中用到比较多。
下面是一个其他的简单例子。
1 |
|
文件的读取与输出
实际上在计算机中都是二进制来储存。
文件在读和写之前都要打开,使用完了要关闭。
fopen_s(文件指针的指针,文件地址,打开模式)
例如:
1 | fopen_s(&stream, "H:\\data.txt", "r"); //以data.txt文件为例 |
fputc(字符,文件指针)
写入一个字符如果失败返回 EOF,end of file 值-1
fgetc(字符,文件指针)
feof(fp)
如果是文件末尾就返回 0。
fgets(字符数组,字节上限,文件流指针)
:如果有回车就停止。
fprintf(文件指针,)
,fscanf()
fweite(接收的指针,每个单元字节数,单元数,文件指针)
区分w,a,r
的模式
函数
字符串赋值:
memcpy(数组a的地址,数组b的地址,操作的字节数)
:其中操作的字节数常用k*sizeof(a[0])
表示复制前面 k 个元素,而sizeof(b)
,就是把b
的所有内容都复制过来,要注意不要使用非法内存。
strcpy(a,b)
:将 b 的内容复制给 a,其中 b 常常是常量字符串。
memset(a, 0, sizeof(a));
:对数组进行初始化赋值 ,用法和上个函数类似,原理也是对内存操作。
sprintf(字符数组的地址,“格式化”,对应变量)
:输出到字符串。
字符串比较:
strcmp(a,b)
:相同则返回 0,a
的字符串大于b
,就返回大于零的数。(即从第一个开始比较,相同字符的减去,然后比较下一个对应的字符,返回a
中这个字符的 ascii 码与b
中对应位置字符的 ascii 码的差值)
字符串拼接:
strcat(a,b)
:将b
的内容拼接到a
的后面,b
自身不变。
查找字符串内容:
char * strchr (const char *str, int c)
: 返回 str
字符串中第一次出现的字符 c
的地址,如果没有这个字符则返回 NULL。
字符属性
int isalnum(字符)
:是否是数字或字母,是的话则返回非 0 的数,否则返回 0;
int isalpha(字符)
:是否为字母。
int isdigit(字符)
:是否为十进制的数字。
int ispunct(字符)
:是否是标点符号。
int islower(字符)
:是否是小写字母。
int isupper(字符)
:是否是大写字母。
字符转换
int tolower(字符)
:成功则返回非零,否则为 0。
int tolower(字符)
:成功则返回非零,否则为 0。
小技巧与知识
!0=1,!1=0
,所以可以用 0 和 1 相对方便的表示出布尔型变量,而且适合在成双成对时进行间隔的操作int a=1;a=!a;
。a==0
或者a==NULL
时执行操作,等价于!a
,如if(a==NULL)
等价于if(!a)
- 如果是简单映射,可以用常量数组来完成。
- 整个地使用字符串用指针,使用多个字符串用指针数组。