1.概念
(1)源文件:源文件即源代码文件,C语言源文件后缀名是.c
(2)头文件:头文件后缀名为.h,C语言代码由源文件和头文件组成。
(3)关键字:关键字是C语言征用的一些字,这些字在C语言中代表特殊含义,已经被C语言
定义好了,轮不到我们用了。我们必须搞懂每个关键字的所有含义和用法,否则就看不懂C语言程序。
(4)注释: //或/* */,注释是给程序员看的,不是给机器看的。编译器编译程序的时候是忽略注释内容的。
(5)符号:C语言中包含很多符号,如; :
;表示C语言语句结束了
(6)变量:会变化的量。C语言程序中用变量来进行计算
(7)函数:函数是C语言的一个基本组成单位,一个C语言程序其实就是由很多函数组成的,每个
函数用来完成一定的功能,函数可以调用别的函数来完成功能。函数的标准是()。
(8)main()函数:入口函数,其他的函数都是直接或间接被main函数调用,从main数开始到main函数结束。
C语言的标准:C89 C99
KISS原则:Keep It Simple, Stupid
四大特点:强大的控制结构,快速,紧凑的代码-程序更小,可移植到其他计算机
C语言的缺点:
(1)代码检查少
例如你定义了int a[10], 你访问a[12]一样是可以访问的,编译运行不会报错。如果a[12]
对应的是一个关键性代码,这时你访问修改a[12]可能就会造成程序的崩溃。其他语言编译器就可能阻止了你访问。
这是C语言很危险的地方。
(2)指针的使用:野指针很危险
(3)C的间接性和丰富的运算符相结合,可能程序很难读懂。
2.关键字
(1)include
使用的时候前面要加#
包含头文件用的
(2)int (integer)定义了整型这个数据类型,用来表示一个整数的类型叫整型。
(3)char (character)字符型数据类型
(4)float 浮点型,用来表示小数的类型
(5)return 返回 函数返回
3.C语言代码步骤
(1)编译源代码(使用VI或者其他编辑器)
(2)编译。编译就是使用编译器把源程序转化成可执行程序的过程,编译要用到编译器。
我们Linux中使用编译器一般是gcc
譬如:gcc hello.c 把当前目录下hello.c文件编译,得到的可执行文件名字叫a.out
也可以自己指定编译后生成的可执行程序的名字,使用gcc hello.c -o hello
(3)执行:执行编译生成的可执行程序,执行方式是./hello
(4)调试
练习题
(1)打印图形
*
***
*****
***
*
方法一:
#include <stdio.h>
int main(int argc, char **argv)
{
printf(" *\n");
printf(" ***\n");
printf(" *****\n");
printf(" ***\n");
printf(" *\n");
return 0;
}
方法二:这种方法不直观
#include <stdio.h>
int main(int argc, char **argv)
{
printf(" *\n ***\n *****\n ***\n *\n");
return 0;
}
方法三:这种方法更精简直观
#include <stdio.h>
int main(int argc, char **argv)
{
printf(" * \ \\需要注意的是:符号\是换行符,\前面有多少个空格都行
\n *** \
\n ***** \
\n *** \
\n * \
\n");
return 0;
}
(2)打印图形
-------------------------------
-------------------------------
**www.link-embed.com**
** Edison **
-------------------------------
-------------------------------
#include <stdio.h>
int main(int argc, char **argv)
{
printf("\n------------------------------- \
\n------------------------------- \
\n **www.link-embed.com** \
\n ** Edison ** \
\n------------------------------- \
\n------------------------------- \
\n");
return 0;
}
总结:
(1)使用到的技术主要是printf中\n换行和\接续符。
(2)C语言中的注释,短的用//,多行的用/**/
(3)熟悉C语言程序的编辑、编译、执行、调试过程。
(4)代码不是一次写完再去编译,而是写一点,编译通过后再去写其他部分的,如果全部写完再来编译
就不好查找错误。
4.C语言数据类型
3.1 整型
C语言中的整型对应数学中的整数,整型变量是用来描述一个整数值的,整型变量经过计算后只能是整数(整型),不可能出现
小数(浮点型,出现小数自动把小数丢弃)。
C语言是一种强类型的语言,变量必须有类型。
整型变量始终是整数,如果你强制给他一个小数,他会自己把小数部分丢掉只剩下整数部分。
C语言中整型有三种:
(1)int 整型
(2)short int 又可简写为short 短整型
(3)long int 又可简写为long 长整型
这三种使用方式相同,不同在于表示范围不同。
3.2 浮点型
对应数学中的小数。浮点型有float和double两种。
使用方式相同,不同在于表示范围和精度。
范围是说表示的数有多大。精度是指这个数的分辨率有多细。
float表示的范围小,精度低。
#include <stdio.h>
#include <string.h>
int main(int argc , char *argv[])
{
float f1 = 3.1415926;
printf("f1 = %f.\n", f1); //f1 = 3.141593. float精度不够,所有给我按照精度取舍了
return 0;
}
#include <stdio.h>
int main(int argc , char *argv[])
{
float f1 = 3.1415926123456789123456789;
printf("f1 = %2.20f.\n", f1);
return 0;
}
3.3 字符型
对应字符,字符就是abcdefg或0123456或+-等等。例如键盘上的这些字符。
写代码要注意一些格式
/*
* Company:Shenzhen link-enbed www.link-embed.com
* Author: Edison Tao
* Project: C language data type - Integer code demonstration
*/
#include <stdio.h>
#include <string.h>
比如海思官方的文件描述:
/******************************************************************
Copyright (C), 2001-2011, Hisilicon Tech. Co. Ltd.
*******************************************************************
File Name : hi_comm_adec.hello
Version : Initial Draft
Author : Hisilicon multimedia software group
Created : 2006/12/15
Last Modified :
Description :
Function List :
History :
1.Date : 2006/12/15
Author : z50825
Modification : Created file
2.Date : 2007/5/10
Author : z50825
Modification : add err code
*******************************************************************/
#include <stdio.h>
int main(int argc , char *argv[])
{
char c1 = 'A';
//printf中,打印字符类型时,使用%d则按照十进制的方式打印,打印出来的是该字符对应
//的ASCII码值;使用%c来打印,则打印出的是该字符的符号。
printf("c1 = %d, c1 = %c\n", c1, c1);
return 0;
}
其实字符型就是一种特殊的整型,比整型的范围小,就是整型的一个子集,比short还short的整型,
只不过这个整型数用来表示了ASCII的编码了而已,每一个值对应了一个字符。
所以它可以和int相运算,
字符型一般用8位二进制表示,无符号字符型范围是0~255,。
#include <stdio.h>
int main(int argc , char *argv[])
{
char c1 = 'A';
printf("c1 = %d, c1 = %c\n", c1, c1);
c1 = c1 + 5; //不同的类型之间按道理是不能加的,c1是一个字符型,5是一个整型,
//但是我们说过,字符型是整型的一个子集
printf("c1 = %d, c1 = %c\n", c1, c1);
return 0;
}
3.4 有符号数和无符号数
数学中数是有符号的,有整数和负数之分。所以计算机中的数据类型也有符号,分为有符号数和
无符号数。
有符号数:
整型:signed int(简写为int)
signed long(简写为long),也可以写作signed long int
signed short(简写为short),也可以写作signed short int
还有种可恶的写法:signed(表示signed int)
浮点型:
signed float(简写为float)
signed double(简写为double)
字符型:
signed char(简写为char)
无符号数:
整型:整型有无符号数,用来表示一些编码编号之类的东西。譬如身份证好,房间号,学号
unsigned int(没有简写)
unsigned long int(简写unsigned long)
unsigned short int(简写为unsigned short)
浮点数:没有无符号浮点数的,小数一般只用在数学概念中,都是有符号的。
字符型:字符型有无符号数,毕竟字符型也是一种整型
unsigned char(没有简写)
总结:其实只有整数才有无符号的,像在使用身份证、学号等的场合,使用无符号数比使用有符号数
在计算机内部效率要高,所有当我们能使用无符号数的时候一般都会优先使用无符号数。因为计算机
还要处理那个符号位,肯定会多耗费性能一下了。
注意:对于整型和符号型来说,有符号数和无符号数表示的范围是不同的。
譬如字符型,有符号数范围是-128~127,无符号数的范围是0~255
C语言的数据类型分成两大类:
(1)内建类型
也叫原生类型,如int、float 、char 、unsigned int ...
(2)自定义类型
结构体、共用体、枚举...
自定义类型都是基于内建类型的
4. C语言常用运算符
4.1 数学运算符号
常见数学运算符号:
+ 加
- 减
* 乘
/ 除,相除以后商
% 取余,相除以后余数是几
头文件包含指令
头文件包含指令#include
--函数声明
--宏定义
--常量定义
使用头文件的好处
1. 避免函数原型的重复声明,利于程序的模块化设计
函数声明可以写到.h文件里面去。
2.避免了宏定义的多次重复声明
可以把宏定义在头文件里面。
3.头文件不会明显增加程序的大小:头文件的内容是编译器需要的信息,而不是加到最终代码里面的具体
语句。
头文件搜索路径
#include <stdio.h> \\尖括号表示到集成开发环境IDE标准路径中去搜索
#include "myfunc.h" \\双引号表示,先到项目工程当前路径去搜索头文件,如果找不到再去IDE标准路径中去找
头文件包含需要注意的一些地方
(1)头文件可以在任何地方使用#include引入
仅仅做文本替换操作,不一定放在文件开头,只要放在调用之前就可以了。
(2)避免多次包含同一个头文件
可以采用条件编译来避免
注意:全局变量一般不放在头文件中
4.C语言常用运算符
4.1 数学运算符号
常见数学运算符号:
+ 加号
- 减号
* 乘号
/ 除号,相除以后的商
% 取余符号,相除以后余数是几
()优先级最高的,先计算
4.1.2 跟数学中意义不同的运算符
= 赋值运算符,与数学中的等号完全不同。赋值运算符作用是经过运算后符号左边的部分(左值,一般是一个变量)的
值就等于右边部分(右值,一般是常数或变量)了。
+= a = a + b; 等同于 a += b;
-= a = a - b; 等同于 a -= b;
*= a = a * b 等同于 a *= b;
/= a = a / b 等同于 a /= b;
%= a = a % b 等同于 a %= b;
这是一种简写,好处是少写了一个变量
4.1.3 判断运算符
== 等于
!= 不等于
> 大于
< 小于
>= 大于等于
<= 小于等于
4.1.4 逗号运算符
逗号运算符的主要作用是用来分割
4.1.5 ++与--
++ a++;等同于++a; a = a + 1; a += 1;
-- a--;等同于--a; a = a - 1; a -= 1;
范例4.1
#include <stdio.h>
int main(void)
{
int a, b, c, d, e;
a = 4;
a++;
printf("a = %d.\n", a); \\a = 5
a = 4;
++a;
printf("a = %d.\n", a); \\a = 5
a = 4;
a = a + 1;
printf("a = %d.\n", a); \\a = 5
a = 4;
a += 1;
printf("a = %d.\n", a); \\a = 5
return 0;
}
思考:++a和a++的区别
#include <stdio.h>
int main(void)
{
int a, b, c, d, e;
a = 4;
b = a++; //先把a的值赋给b后,a再加1
printf("a = %d.\n", b);
a = 4;
b = ++a; //a的值先加1,然后在把加1后a的值赋给b
printf("a = %d.\n", b);
}
注意:强调程序风格
所谓程序风格,主要是指代码的格式,譬如空格,空行,缩进,注释,文件头,函数头等。
越是大公司,越看重程序风格。软件界公认的准则:程序风格不良好的程序是垃圾代码,是
废品,写出程序不良好的代码的人,是垃圾程序员。
5. 程序结构
在C语言程序里,一共有三种程序结构:顺序结构、选择结构(分支结构)、循环结构
顺序结构:按照实物本身特性,必须一个接着一个来完成。
选择结构:到某个节点后,会根据一次判断结果来决定之后走哪一个分支。
循环结构:循环结构有一个循环体,循环体是一段代码。对于循环结构来说,关键在于
根据判断的结果,来决定循环体执行多少次。
总结:对于顺序结构来说,不需要判断,因为下一句指令就是你要执行的。对于循环与选择结构来说,
都需要进行判断。然后根据判断结果来决定怎么办。
逻辑上有一种类型,叫bool类型(又写作boolean类型,中文叫布尔类型)。布尔类型只有两个值,真和假。
在C语言中有以下一些判断运算符
== 等于
!= 不等于
> 大于
< 小于
>= 大于等于
<= 小于等于
使用这些判断运算符,可以写出一个判断表达式,这个判断表达式最终的值就是一个bool类型。
这个判断表达式
选择结构详解:
C语言中选择结构一共有两种:
第一种:if else
引入关键字: if else else if
if (bool值) //如果bool值为真,则执行代码段1,否则执行代码段2
{
代码段1
}
else
{
代码段2
}
if (bool值1) //如果bool值1为真,则执行代码段1
{ //否则判断bool值2是否为真,若为真则执行代码段2
代码段1 //否则直接执行代码段3
}
else if (bool值2) //开头的if和结尾的else都只能有一个,但是中间的
{ //else if可以有好多个
代码段2
}
else
{
代码段3
}
范例:
#include <stdio.h>
int main(void)
{
int a, b, max;
a = 333;
b = 333;
if (a > b)
{
max = a;
printf("true"); //调试方法:在不同分支或者需要处自定打印一些数据
} //然后运行后根据打印内容,来分析程序的实际走向
else //和运行情况,以此来做调试分析。
{
max = b;
printf("false\n");
}
printf("max = %d\n", max);
return 0;
}
第二种:switch case
先看一个例子
/*
*Company :Kuang-Chi
*Author :Edison Tao
*Project : Switch case
*/
#include <stdio.h>
//title: type number 1,print a; type 2, print b,until z.
int main(void)
{
int num;
num = 14;
if (num == 1)
{
printf("a\n");
}
else if (num == 2)
{
printf("b\n");
}
else if (num == 3)
{
printf("b\n");
}
else
{
printf("--------\n");
}
return 0;
}
这个程序如果从头写到尾能有六七十行那么长,全部都是这些if else if else的这种结构
首先是代码不好看,代码的目的也不明确
像这种问题我们一般通过switch语句来实现
//title: type number 1,print a; type 2, print b,until z.
int main(void)
{
int num;
num = 7;
//use switch case to complete the function
switch (num)
{
case 1:
printf("a\n");
break;
case 2:
printf("b\n");
break;
case 3:
printf("c\n");
break;
case 4:
printf("d\n");
break;
case 5:
printf("e\n");
break;
case 6:
printf("f\n");
break;
case 7:
printf("i\n");
break;
case 8:
printf("j\n");
break;
default:
printf("--------\n");
break;
}
return 0;
}
switch语句的格式:相当于一个一对多的一个开关,只要前面的匹配上后面的都没戏了
这种分支很像if else语句,default很像最后的那个else
涉及到的C语言的关键字还很多,switch case break default
注意:
1. case后面是一个常量(不能是变量),这个常量是一个整型的(不能是float,double可以是int char),
不能用小数(涉及到精度问题).
2. 一般来说,每个case中代码段后都必须有一个break;如果没有,结果可能会让你大吃一惊
3. case之后一般都会有default。语法上允许没有default,但是建议写代码时一定要写。
switch (变量) //执行到这一句时,变量的值已经知道了
{
case 常数1: //switch case语句执行时,会用该变量的值一次与各个case后的常数去对比,试图
代码段1: //找到第一个匹配项。找到匹配的项目后,就去执行该case对应的代码段
break; //如果没找到则继续下一个case,直到default.
case 常数2: //如果前面的case都未匹配,则default匹配
代码段2;
break; //注意每一个case都有一个break
......
default:
代码段2;
break;
}
switch case和if else对比:
(1). if else适合对比条件比较复杂,但是分支比较少的情况;
switch case适合那种对比条件不复杂啊,但是分支数很多的情况。
(2). 所有的选择结构,其实都可以用if else来实现。但是只有部分才
可以用switch case实现。一般的做法是:在适合使用switch case的情况下
会优先使用switch case,如果不适合使用switch case,则不得不使用if else。
switch case的调理性,清晰性要比if else好很多。如果能用switch case 我是
绝对不能if else的。
注意:case里面是不能初始化变量,究其根本原因,是C的一条规则:在任何作用域内,
假如存在变量初始化语句,该初始化语句不可以被跳过,一定要执行。
5.2 C语言中的循环结构
C语言中常用的循环结构有三个:for循环、while循环、do while循环。
5.2.1 for循环
for (循环控制变量初始化;循环终止条件;循环控制变量增量)
{
循环体
}
for循环的执行步骤:
(1)先进行循环控制变量初始化
(2)执行循环终止条件,如果判断结果为真,则进入第3步;
如果为假则循环终止,退出。
(3)执行循环体
(4)执行循环控制变量增量转入第(2)步。
范例:
#include <stdio.h>
int main(void)
{
int a, sum;
sum = 0;
for (a=1; a<=56; a++)
{
sum = sum + a;
}
printf("sum = %d.\n", sum);
return 0;
}
#include <stdio.h>
int main(void)
{
int i;
for (;;); //可以,死循环
return 0;
}
#include <stdio.h>
int main(void)
{
int i; //当我们定义了一个局部变量,但是没有初始化的时候,这个值是随机的
printf("i = %d.\n", i);
for (; i<17; i++)
{
printf("i = %d.\n", i);
}
}
#include <stdio.h>
int main(void)
{
int i;
printf("i = %d.\n", i);
i = 0; //循环控制变量初始化可以放在外面的,因为它只执行一次,不参与循环
for (; i<17; i++)
{
printf("i = %d.\n", i);
}
}
#include <stdio.h>
int main(void)
{
int i;
printf("i = %d.\n", i);
for (i=0; i<17; i++)
{
printf("i = %d.\n", i);
}
}
#include <stdio.h>
int main(void)
{
int i;
printf("i = %d.\n", i);
for (; i<17;) //增量i++去掉会进入死循环,条件永远满足
{
printf("i = %d.\n", i);
}
return 0;
}
#include <stdio.h>
int main(void)
{
int i;
for (i=0; i<17;)
{
printf("i = %d.\n", i);
i++; //放在这里与放原来的位置执行效果是一样的
}
return 0;
}
#include <stdio.h>
int main(void)
{
int i;
for (i=0; i<17; i++)
{
printf("i = %d.\n", i);
i++; //i每次加了两次,打印出来的是偶数
}
return 0;
}
#include <stdio.h>
int main(void)
{
int i;
int sum;
for (i=0,sum=0; i<17; i++) //初始化表达式里面允许有多个,用逗号隔开即可
{
sum +=i;
}
printf("sum = %d.\n", sum);
return 0;
}
注意:
1、for循环中()中三部分可不可以省略?
标准的for循环,应该把循环控制变量的初始化,增量都放在()当中,并且在循环体中绝对不应该
更改循环控制变量(可以引用它的值,但不应该改变它)。
2. 养成for与()之间加空格、for()里面分号后面加空格(让别人一眼就看出来这是三部分
),变量与符号之间不加空格的习惯。
3.千万不要省略循环体的那对括号
4. 循环体离{}距离为一个tab
范例:10的阶乘运算
#include <stdio.h>
int main(void)
{
int i, val;
for (i=1,val=1; i<=10; i++) //一般程序员都不会这么写,for的括号里面一般都不会用<=或>=
{
val *=i;
}
printf("val = %d.\n", val);
return 0;
}
#include <stdio.h>
int main(void)
{
int i, val;
for (i=1,val=1; i<11; i++) //for的括号里面一般都不会用<=或>=,便于你去判断这个循环可以执行多少次
{ //这是一种习惯,大多数人写代码都是这么写的
val *=i; //一般i都是从0开始,阶乘是特殊情况,只能从1开始,如果从0开始,一眼就看出执行了11圈
printf("i = %d.\n", i); //调试用,非常有效,学会调试对你来说是一个飞跃
}
printf("val = %d.\n", val);
return 0;
}
#include <stdio.h>
int main(void)
{
int i, val;
for (i=10,val=1; i>=1; i--) //倒着来,看自己的习惯
{
val *=i;
printf("i = %d.\n", i);
}
printf("val = %d.\n", val);
return 0;
}
注意:val *= i;要习惯这种写法,val = val * i;这种写法也可以,一般新手才会这么写,以后要过度到val *= i;
范例 :打印一张ASCII码表
#include <stdio.h>
int main(void)
{
int i;
printf("-------ASCII-------\n");
for (i=0; i<128; i++)
{
printf("\t%d %c\n", i, i);
}
printf("-------ASCII-------\n");
return 0;
}
5.2.2 while循环
while 循环的执行步骤:
(0)首先是循环初始化。这一步其实不属于while循环本身。
(1)先判断终止条件是否满足。如果是真,则进入第2步;否则直接退出。
(2)执行循环体,然后转入第1步。
while 循环不如for循环,为什么?
(1)循环初始化条件放在了外面,不整齐,容易忘掉。循环本身不提醒你有这么一部分。
(2)循环控制增量在循环体里面,让你比较困惑,循环体简单还好说,如果复杂了就比较难搞。
所以不如for循环那么简洁明了。
范例:
/*
*Company :Kuang-Chi
*Author :Edison Tao
*Project : while loop
*Function: the sum of odd number from 1 to 100
*/
#include <stdio.h>
int main(void)
{
int i, sum;
i = 1;
sum = 0; //循环初始化,其实是while循环之外的
while (i < 100) //终止条件
{
sum += i;
printf("i = %d.\n", i);
i += 2; //这三行为循环体,本行为循环控制增量,属于循环体的一部分
}
printf("sum = %d.\n", sum);
return 0;
}
5.2.3 do while循环
范例:
/*
*Company :Kuang-Chi
*Author :Edison Tao
*Project : while loop
*Function: the sum of odd number from 1 to 100
*/
#include <stdio.h>
int main(void)
{
int i, sum;
i = 1;
sum = 0; //初始化条件
do
{
sum += i;
printf("i = %d.\n", i);
i += 2; //增量,循环体的一部分
}while (i < 100); //终止条件,注意这里有个分号
printf("sum = %d.\n", sum);
return 0;
}
do while 循环的执行步骤:
0. 首先是循环初始化。这一部分其实不属于do while循环体本身。
1. 执行循环体(循环控制变量的增量是循环体的一部分)
2. 判断终止条件。若成立,则转入1;若不成立则退出。
总结:不管哪种循环结构,都不能缺少一些要素:
循环控制条件初始化,终止条件,循环控制变量增量,循环体。
不同的循环方式(for和while和do while)都有这些,只是格式不同,表现形式不同,
放的地方不同
while循环和do while循环哪里不同?while循环是先判断后执行,do while循环是先执行后判断,
等循环开始转了之后,其实是一样的。do while用的很少。
理解和记忆是相互促进的。如果看不懂,就去记,记住了自然就明白了。如果记不住,就去理解。
理解了自然就记住了。
while循环有没有可能循环体一次都不执行? 有 一上来条件就不满足的情况
do while循环有没有可能循环体一次都不执行? 不可能,因为一上来就执行一次,至少会执行一次
基础知识:
当我们定义了一个局部变量,但是没有初始化的时候,这个值是随机的。
当我们定义了一个全局变量,但是没有初始化的时候,这个值是0。
C语言基础大模块:
数据类型
运算符
三种程序结构
函数
数组
指针
结构体、共用体、枚举
6. 函数
截至目前为止,已经学习的数据类型,运算符,三种程序结构,已经可以完成一些C语言程序了。但是
不足之处在于写简单程序可以,写不了复杂程序。
当程序简单的时候,一个人可以用一个main函数搞定功能。当程序变成复杂的时候,超出了人的大脑承受范围,这时候逻辑不清了。
这时候就需要把一个大程序分成许多小的模块来组织,于是乎出现了一个概念叫函数。
函数是C语言代码的基本组成部分,它是一个小的模块,整个程序由很多个功能独立的模块(函数)组成。
这就是程序设计的基本分化方法。
之前接触过的函数:
main: C语言中所谓的主函数,主函数就是一种特别的函数。一个C语言程序只能
有且必须有一个main函数。为什么?
C语言规定,一个C语言程序从主函数开始执行,到主函数执行完结束。
printf():函数的作用是用来在标准输出中打印信息。这个函数不是程序员自己写的,是C语言的
标准库提供的一个库函数。在C语言中写代码时可以引用库函数,但是必须使用#include引用这个库
函数所在的头文件。
基础知识:
生命周期,指一个东西从出生到死亡的过程。
/*
*Company :Kuang-Chi
*Author :Edison Tao
*Project : Function
*Function:
*/
#include <stdio.h>
//函数声明,注意函数声明后面有分号,定义的时候没有分号
int add(int a, int b);
int sub(int a, int b);
int multiply(int a, int b);
int divide(int a, int b);
int main(void)
{
int a, b, c;
a = 23;
b = 122;
//函数调用,调用已经写好的函数模块,来完成既定功能
c = add(a, b);
c = sub(a, b);
c = multiply(a, b);
c = divide(a, b);
printf("c = %d.\n", c); // printf("a + b = %d.\n", add(a, b));可以这么直接调用,编译器允许这么做
return 0;
}
//函数定义,又叫函数实现
int add(int a, int b)
{
return a + b; //函数体,实际上就是函数的实现
}
//函数定义,又叫函数实现
int sub(int a, int b)
{
return a - b;
}
//函数定义,又叫函数实现
int multiply(int a, int b)
{
return a * b;
}
//函数定义,又叫函数实现
int divide(int a, int b)
{
return a / b;
}
6.1 使用函数来写程序时的关键部分
函数定义:函数定义是关键,是这个函数的实现。函数定义中包含了函数体,函数体中的代码段决定了这个函数
的功能。
函数声明:函数声明实际上是函数原型声明。什么叫原型?函数的原型包含三部分:函数名,返回值类型,
函数参数列表。通俗讲,函数原型就是这个函数叫什么,接收什么类型的几个参数,返回一个什么样的返回值。
函数声明的作用,在于告诉使用函数的人,这个函数使用时应该传递给他什么样的参数,它会返回什么样类型
的返回值。这些东西都是写函数的人在函数定义中规定好的,如果使用函数的人不参照这个原型来使用,就会
出错,结果就会和你想的不一样。
函数调用:使用函数名来调用函数完成功能。调用时必须参照原型给函数传参,然后
6.2 函数的参数:
形参:形式参数,一个概念而已,一个象征意义,在函数定义和函数
实参:实际上的参数,一个变量,函数调用中,实际传递的参数才是实参。
c = sub(b, a); //实参给形参传参的时候,按顺序传参的,不是名字
//就是说,实参的第一个参数实际传给了形参列表的第一个参数,而实参的第二个参数实际
//传给了形参列表的第二个参数。以此类推。
//实参的名字和形参的名字一点关系都没有。管你一样还是不一样。只是按照顺序依次赋值传参而已。
//实参的类型必须和形参类型相同,否则就可能会出错。
函数调用的过程,其实就是实参传给形参的一个过程。这个传递实际就是一次拷贝。实际传参的时候
,实参(本质是一个变量)本身并没有进入到函数内,而是把自己的值复制了一份传给了函数中的形参,
在函数中参与运算。这种传参方法,就叫传值调用。
6.3 返回值:
当函数执行完之后,会给调用该函数的地方返回一个值。这个值的类型就是函数声明中返回值类型,
这个值就是函数体中最后一句return xxx返回的那个值。
6.4 函数名,变量名
第一点:起名字时候不能随意,要遵守规则。这个规则有两个层次;
第一层就是合法,第二层是合理。合法就是符号C语言中变量名的命名规则。
合理就是变量名起的好,人一看就知道什么意思,一看就知道这个函数是干嘛的,
而且优美、好记。
第二点:C语言中,所有的符号都是区分大小写的。也就是说abc和Abc和aBc不一样
第三点:C语言函数名变量名的命名习惯。没有固定的结论,有多种使用都很广泛的命名方式。
介绍两种这里,
一种是Linux的命名习惯:student_age str_to_int
另一种是驼峰命名法:studentAge StrToInt
作业:
自学程序风格。空格 空行 缩进......
<<高质量程序设计指南>> 作者:林锐
华为代码规范
hisilicon海思半导体 HI_xxxx
9. 结构体、共用体、枚举、宏定义、预处理
9.1 结构体
9.1.1 为什么需要结构体?什么是结构体?
变量+数组
没有结构体之前,C语言中,数据的组织依靠:变量+数组。
最初最简单的时候,只需要使用基本数据类型(int char float double)来
定义单个变量,需要几个变量就定义几个。
后来情况变复杂了,有时需要很多意义相关的变量(譬如需要存储及运算一个班级的学生分数)
这时候数组出现了。数组解决了需要很多类型相同、意义相关的变量的问题。
但是数组是有限制的。数组最大的不足在于,一个数组只能存储很多个数据类型相同的变量。
所以碰到需要封装几个类型不同的变量的时候,数组就无能为力。
这时候就需要结构体。
题目:使用一个数据结构来保存一个学生的所有信息:姓名 学号 性别
7. 数组
到目前为止,我们已经学习了C语言的基本数据类型:整型、浮点型、字符型(基础因子)。
再往后就是复合数据类型(组合起来的)。
所谓复合数据类型,是指由简单数据类型,经过一定的数据结构封装,组合而成的新的数据类型。
譬如数组、譬如结构体、譬如共用体。
7.1 为什么需要数组?
计算所有同学的分数,年龄
(1)变量的初始化
#include <stdio.h>
int main(void)
{
//第二种,定义的同时初始化(什么叫初始化,定义的同时给它一个值就叫初始化,如果不是同时就叫赋值了)
int a = 23;
//第一种,先定义变量,再显式赋值
int a; //定义了一个int变量a,但是没有初始化
a = 23; //这叫赋值,不叫初始化(因为它是在定义之后再操作的)
printf("a = %d.\n", a);
return 0;
}
//定义的同时初始化的好处是不会忘了赋值
总结:
1.一般来讲,只要你记得显示赋值,则两种方式并无优劣差异。但是人会犯错,会不小心,
所以还是定义同时初始化好一点,因为这个定义的时候有了固定值,即使之后忘记显示赋值也
不会造成结果是随机的。
2. 一般情况下,定义的同时都将变量初始化为0。局部变量定义同时初始化为0,这是一个写
代码好习惯。
(2)数组的初始化
第一种:完全初始化。依次赋值
第二种:不完全初始化。初始化式中的值a[0]开始,依次向后赋值,不足的默认用0填充赋值
#include <stdio.h>
int main(void)
{
int i = 0;
int a[3] = {1, 4, 9}; //完全初始化
for (i=0; i<3; i++)
{
printf("a[%d] = %d.\n", i, a[i]);
}
return 0;
}
#include <stdio.h>
int main(void)
{
int i = 0;
int a[3] = {33}; //不完全初始化,a[0] = 33,后面两个是0
for (i=0; i<3; i++)
{
printf("a[%d] = %d.\n", i, a[i]);
}
return 0;
}
#include <stdio.h>
int main(void)
{
int i = 0;
int a[3] = {}; //不完全初始化,三个元素全部为0
for (i=0; i<3; i++)
{
printf("a[%d] = %d.\n", i, a[i]);
}
return 0;
}
#include <stdio.h>
int main(void)
{
int i = 0;
int a[3]; //这种情况是没有初始化,三个元素的值是随机的
for (i=0; i<3; i++)
{
printf("a[%d] = %d.\n", i, a[i]);
}
return 0;
}
#include <stdio.h>
int main(void)
{
int i = 0;
int a[3] = {[1] = 10}; //这种初始化是gcc特有的,指定某个元素的值为多少,其他元素不管
for (i=0; i<3; i++)
{
printf("a[%d] = %d.\n", i, a[i]);
}
return 0;
}
#include <stdio.h>
int main(void)
{
int i = 0;
int a[5] = {[0] = 100, [4] = 99}; //a[0]指定为100,a[4]指定为99
for (i=0; i<5; i++)
{
printf("a[%d] = %d.\n", i, a[i]);
}
return 0;
}
#include <stdio.h>
int main(void)
{
int i = 0;
int a[5] = {100, [4] = 99}; //还可以混合起来用
for (i=0; i<5; i++)
{
printf("a[%d] = %d.\n", i, a[i]);
}
return 0;
}
#include <stdio.h>
int main(void)
{
int i = 0;
int a[5] = {100, 20, 30, [4] = 99}; //多几个也行
for (i=0; i<5; i++)
{
printf("a[%d] = %d.\n", i, a[i]);
}
return 0;
}
基础知识 + 推断能力
学习 = 基础知识 + 合理推论
7.2 数组元素之间指针的差值为多少
相邻元素之间指针的差值具体是多少,就要看数组的类型,比如,如果数组的类型int型的
话,你每个元素的空间的大小为4个字节,那么元素之间的指针差值就是4
如果说是double型的话,数组的每个元素应该为8个字节,那么相邻元素之间的指针的差应该是8
7.3 数组元素指针+1,怎么理解
数组元素指针+1,加的不是一个字节,
+1,加的是一个元素空间的大小,比如这个元素空间为int型的话,那么+1应该+4个字节
前一个元素的指针+1后,就得到紧挨着的后一个元素的指针
同理后一个元素的指针-1后,就得到了前一个元素的指针
7.4 得到了元素的指针后,如何访问这个指针所指向的空间
(a)回顾一下使用指针访问
7.5 不同数据类型数组
int a[5]; //整型数组
float a[5]; //浮点型数组
double a[5]; //双精度浮点型数组
char a[5]; //字符数组
程序在环境中运行时,需要一定的资源支持。这些资源包括:CPU(运算能力)、
内存等,这些资源一般由运行时环境(一般是操作系统)来提供,譬如我们在Linux
系统上./a.out运行程序时,linux系统为我们提供了运算能力和内存。
程序越庞大,运行时消耗的资源越多。譬如内存占用,越大的程序,占用的内存越多。
占用内存的其中之一,就是我们在程序中定义的变量。
C语言程序中,变量的实质就是内存中的一个格子。当我们定义(创造一个变量)了一个变量
后,就相当于在内存中得到了一个格子,这个格子的名字就是变量名,以后访问这个内存格子就
使用该变量名就行了。这就是变量的本质。
数据类型的实质是内存中格子的不同种类。譬如在32位机器上
整型格子(类型是int)、 占用4字节空间
单精度浮点型格子(float)、 占用4字节空间
双精度浮点型格子(double)、 占用8字节空间
字符型格子(char)。 占用1字节空间
非常像塑造东西的模子。模子
长什么样,塑造出来的东西就长什么样。内存本身是没有类型的。
7.5.1 数组的传参
void fun_buf(int n, int buf[]) //int buf[]这种写法虽然言简意赅,但是不利于我们理解本质要做什么
//这个类型int buf[]你根本就看不懂是啥类型
int buf[5] = {0, 1, 2, 3, 4};
fun_buf(5, buf); //buf等价于&buf[0]
总结: C语言提供的数组传参的普通写法是,实参写数组名,形参在接收数组的时候,形参写成
类型 形参名[n]//[]的数字无所谓,写多大无所谓,写与不写没有任何关系
int buf[]
当然,我们传递数组,记得还要将数组的元素个数传递给被调用函数。
7.5.2 数组传参时传递的是什么
我们在前面说过,对应C语言的传参来说,不管是什么样的传参,都是将实参的值传递给
形参,这个过程其实就是将实参的值赋值给形参。
1)第一种选择:将数组所有的元素值,全部赋值给形参
假如你的实参涉及数组有100元素,也就意味你的形参也必须开辟出100个元素空间,
用来存放从实参赋值过去的所有的数组元素值。
(a) 浪费空间
(b) 浪费时间,复制过程非常浪费时间
如果你是C语言的语法设计者,你会选择这种传参方式吗,所以C语言使用并不是
我们假设的这种传参方式,而是传递的是指针。
2)第二种:将数组第一个元素的指针赋值给形参
既能节省空间,同时也能节省时间,何乐不为,所以当初设计C语言前辈,毅然决然
的选择了使用指针方式,对数组进行传参。
int *型
fun_buf(5, buf); //buf等价于&buf[0]
fun_buf2(int n, int *p) //int *p等价于int p[]
//在int p[]这个写法中,p实际上就是一个int *的指针变量
注意:为什么C语言要提供int p[],直接使用int *p不就好了吗,因为C语言是一个人性化的语言,
很多初学者对于int *p写法不理解,因此C语言才提供了int p[],让你一看就知道传递
的是一个数组,因为有个[],但是这个写法不好在于,很难理解数组传参的本质。
为什么在int p[]这个写法中,[]中的数字没有任何意义,本质就是int *p,不管你给
的数字多大,反正它只开辟一个固定大小的指针变量空间,用于存放数组的指针。
void fun_buf2(int n, int *p)
{
int i = 0;
for(i=0; i<5; i++)
{
printf("%d\n",*(p+i));
printf("%d\n", p[i]); //也可以,p[i]与*(p+i)完全等价
}
}
把数组元素个数给那个函数,再把数组的指针给那个函数,则那个函数就可以访问这个数组了
总结:数组传参传的是指针
7.6 sizeof运算符
作用:返回一个变量或者一个数据类型的内存占用长度,以字节为单位。
sizeof返回的变量或者数据类型的在内存的长度与编译器的的位数有关(32位或者64位)
与操作系统(运行环境)的位数无关,比如在64位的操作系统上装32位的gcc和64位的gcc返回的内存占用
长度就不一样。
定义 int a[0]; gcc编译器没有报错(对C语言有扩展,本来C语言是不允许定义0长度的数组的),微软的编译器报错
结论:不同的编译器对C语言的支持标准是不一样的,而且不同的编译器对C语言有扩展。
例如:Linux内核的程序一般是用gcc来编译的,如果用其他的编译器来编译很难通过。
这就是编译器支持的C标准和编译器扩展带来的问题。
跨平台开发就是要解决不同编译器的对C语言扩展的部分。
#include <stdio.h>
//测试类型
int main(void)
{
printf("sizeof(int) = %d.\n", sizeof(int)); //4
printf("sizeof(float) = %d.\n", sizeof(float)); //4
printf("sizeof(float) = %d.\n", sizeof(double)); //8
printf("sizeof(float) = %d.\n", sizeof(char)); //1
return 0;
}
//变量测试
#include <stdio.h>
int main(void)
{
double d;
int a;
int len;
len = sizeof(d);
printf("len = %d.\n", len); //8
return 0;
}
//测数组
#include <stdio.h>
int main(void)
{
int len;
int a[5];
len = sizeof(a);
printf("len = %d.\n", len); //4*5=20
return 0;
}
#include <stdio.h>
int main(void)
{
int len;
int a[5];
len = sizeof(a);
len = sizeof(a) / sizeof(a[0]); //测试数组元素的个数
printf("len = %d.\n", len);
return 0;
}
7.7 字符数组及它的两种初始化
为什么要单独讲一下字符数组:
基础知识:
1. 在C语言中引用一个单个字符时,应该用单引号''括起来。
2. 定义数组同时初始化,则可以省略数组定义时[]中的长度。C语言编译器会自动推论
其长度,推论依据是初始化式中初始化元素的个数。由此可知,省略[]中数组元素个数只有一种情况,
那就是后面的初始化必须为完全初始化。
3. C 语言中引用一个字符串时,应该用""括起来,譬如"abcde"
"abcde"实际上有6个字符,分别是'a' 'b' 'c' 'd' 'e' '\0'
'\0'这个字符是ASCII码表中的第一个字符,它的编码值是0,对应的字符时空字符(不可见字符,
在屏幕上看不见,没法显示,一般要用转义字符方式来显示。譬如'\n'表示回车符,
'\t'表示Tab, '\0'代表空字符)。
'\0'是C语言中定义的字符串的结尾标志。所以,当C语言程序中使用"abcde"这种方式去初始化
时,编译器会自动在字符'e'后面添加一个'\0'。于是乎变成了6个字符。
#include <stdio.h>
//定义一个包含5个字符的字符数组
int main(int argc, char **argv)
{
char a[5] = {'a', 'b', 'c', 'd', 'e'}; //定义并初始化一个字符数组
int i = 0;
for (i=0; i<5; i++)
{
printf("a[%d] = %d %c.\n", i, a[i], a[i]);
}
return 0;
}
#include <stdio.h>
int main(int argc, char **argv)
{
char a[5] = {97, 98, 99, 100, 101}; //这种定义与上面的效果一样
int i = 0;
for (i=0; i<5; i++)
{
printf("a[%d] = %d %c.\n", i, a[i], a[i]);
}
return 0;
}
#include <stdio.h>
int main(int argc, char **argv)
{
char a[] = {97, 98, 99, 100, 101}; //这种也行,编译器根据初始化知道数组元素的个数
int i = 0;
for (i=0; i<5; i++)
{
printf("a[%d] = %d %c.\n", i, a[i], a[i]);
}
return 0;
}
#include <stdio.h>
int main(int argc, char **argv)
{
char a[] = "abcde"; //字符串方式来初始化字符数组,与上面结果是一样的,这种最简单,用的也是最多的
int i = 0; //高手都是这种写法
for (i=0; i<5; i++)
{
printf("a[%d] = %d %c.\n", i, a[i], a[i]);
}
return 0;
}
#include <stdio.h>
int main(int argc, char **argv)
{
int i = 0;
char a[] = {97, 98, 99, 100, 101};
char b[] = "abcde";
printf("sizeof(a) = %d, sizieof(b) = %d.\n", sizeof(a), sizeof(b)); //sizeof(a) = 5, sizieof(b) = 6.
return 0;
}
8. 指针
指针全称是指针变量,其实质是C语言的一种变量。这种变量比较特殊,通常它的值
会被赋值为某个变量的地址值(例如p = &a),然后我们可以使用*p这样的方式去间
接访问p所指向的那个变量。
8.1 为什么需要指针?
指针存在的目的就是间接访问。有指针之后,我们访问变量a不必只通过a这个变量名来
访问。而可以通过p = &a; *p = xxx;这样的访问方式间接访问变量a。
举个例子:有8间房,每一个房间都有一把钥匙,一种方式就是我身上随身带着这8间房的
8把钥匙,想开哪个开哪个,这是一种最直接的方式。
实际上我们不是这么做的,我们一般是把其他7间房的钥匙串成一串,放到第8间房里面,
然后把第8间房的钥匙带在身上,想进第7间房的时候先到第8间房把那一串钥匙取出来,然后
再打开第7间房。想的那么多干嘛,就是钥匙太重了,天天把那么多的钥匙带在身上类啊,
如果是一百间房的钥匙呢?很少要每天去同时开8间房的,为了节省空间,现实生活就是
这么做的,编程中也是一样的道理,间接访问的时候就需要指针。
8.2 指针的定义和初始化
指针既然是一种变量,那么肯定也可以定义,也可以初始化。
&:取地址符,将它加在某个变量前面,则组合后的符号代表这个变量的地址值。
例如:int a; int *p; p = &a; 则将变量a的地址值赋值给p。
就在上面的例子中,有以下一些符号:
a 代表变量a本身
p 代表指针变量p本身
其实a和p没有本身区别,都是一个变量,只是变量类型不一样而已
&a 代表变量a的地址值, p = (&a); p中存的是变量a的地址
*p 代表指针变量P所指向的那个变量,也就是变量a,所有*p = 111;相当于a = 111;
*:指针符号。指针符号在指针定义和指针操作的时候,解析方法是不同的。
int *p; //p是一个指针变量,该指针指向一个整型数
使用指针的时候,*p则代表指针变量p所指向的那个变量
#include <stdio.h>
int main(void)
{
int a = 23;
int b = 0;
int *p;
p = &a; //变量p指向变量a
//*p = 111; //*p的值就是p所指向的那个变量,这里是变量a
b = *p;
printf("a = %d.\n", a);
printf("b = %d.\n", b);
printf("p = %p.\n", p); //%p用于打印指针变量的值,值是一个地址,例如p = 0x7ffdaffa1e58.
return 0;
}
上面的例子中,有以下一些符号:
a 代表变量a本身
p 代表指针变量p本身
&a 代表变量a的地址
*p 代表指针变量p所指向的那个变量,也就是变量a
&p 代表指针变量p本身的地址值
8.3 指针的定义和初始化
指针既然是一种变量,那么肯定也可以定义,也可以初始化。
第一种:先定义再赋值
int *p; //定义指针变量p
p = &a; //给p赋值
第二种:定义的同时初始化
int *p = &a; //效果等同于上面的两句
#include <stdio.h>
int main(void)
{
int a = 23;
int *p;
*p = &a; //错误
printf("a = %d.\n", a);
return 0;
}
8.4 各种不同类型的指针
指针变量本质上是一个变量,指针变量的类型属于指针类型。(指针本身就是一种类型)
int *p;定义了一个指针类型的变量p,这个p指向的那个变量是int型。
#include <stdio.h>
int main(void)
{
float a = 23;
int *p = &a; //错误,指向类型不匹配
printf("a = %d.\n", a);
return 0;
}
指针是有各种各样的类型的
int *p; //p是指针类型,指向的变量是int类型
char *p; //p是指针类型,指向的变量是char类型
float *p;
double *p;
各种指针类型和它们指向的变量类型必须匹配,否则结果不可预知
8.5 指针定义的两种理解方法
int *p;
第一种:首先看到p,这个变量名,其次,P前面有个*,说明这个变量p是
一个指针变量,最后,*p前面有一个int,说明这个指针变量p所指向的是
一个int型数据。
char *(*pfunc)(char *, char *)
第二种:首先看到p,这个是变量名;其次,看到p前面的int *,把int *作为一个
整体来理解,int *是一种类型(符合类型),该类型表示一种指向int型数据的指针,
int *p1; //没错
int* p2; //可以
int * p3; //可以 三种写法效果一样
#include <stdio.h>
int main(void)
{
int a = 1111;
int *p1;
p1 = &a;
*p1 = 321;
printf("a = %d.\n", a); //321
return 0;
}
#include <stdio.h>
int main(void)
{
int a = 1111;
int* p1;
p1 = &a;
*p1 = 321;
printf("a = %d.\n", a); //321
return 0;
}
#include <stdio.h>
int main(void)
{
int a = 1111;
int * p1;
p1 = &a;
*p1 = 321;
printf("a = %d.\n", a);
return 0;
}
8.6 指针与数组的初步结合
#include <stdio.h>
int main(void)
{
int a[5] = {555, 444, 333, 222, 111};
int *p;
p = &a; //incompatible pointer type 编译报警告,指针类型不兼容,因为a是一个数组类型的变量
//而p是int类型的,应该赋给一个数组指针才对
printf("*p = %d.\n", *p); //输出555
return 0;
}
#include <stdio.h>
int main(void)
{
int a[5] = {555, 444, 333, 222, 111};
int *p;
p = &a[0]; //相当于p = &(a[0]),编译没有报警告是因为a[0]是int类型的数
printf("*p = %d.\n", *p); //输出555
return 0;
}
#include <stdio.h>
int main(void)
{
int a[5] = {555, 444, 333, 222, 111};
int *p;
p = a; //编译没有错误没警告,执行没有错,打印555
printf("*p = %d.\n", *p);
return 0;
}
数组名:做右值时,数组名表示数组的首元素首地址,因此可以直接赋值给指针
如果有 int a[5];
则 a和&a[0]都表示数组首元素a[0]的首地址,一样的。
而&a则表示数组的首地址。
注意:数组首元素的首地址和数组的首地址是不同的。这两个意义不一样但在数值上是相等的。
前者是数组元素的地址,而后者是数组整体的地址。
举例来说明:例如你们家是你们村的第一户,你们家的门牌号是500号,我们可以讲500这个数
字是你们家的门牌号,也可以讲500这个数字是你们村的第一个门牌号(整个村起始门牌号),
数字是一样的,但是意义是不一样的。
#include <stdio.h>
int main(void)
{
int a[5] = {555, 444, 333, 222, 111};
int *p;
p = a;
a = p; //报错,数组名做左值时是一个常量,数组名本来就是一个常量,是不能给它赋值的
//它是数组首元素的首地址,所以不能做左值,只能做右值
printf("*p = %d.\n", *p);
return 0;
}
所以我们用指针指向数组后,就可以用指针去把数组里面的值一个一个的取出来
根据以上,我们知道可以用一个指针指向数组的第一个元素(当然也可以指向第二个元素,第三个...),
这样就可以用间接访问的方式去访问数组中各个元素。
这样访问数组就有了两种方式。
有 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运算是以
数据类型为单位的。int类型的数组对应的指针加1就是往前挪4个字节。
8.7 指针与++ --符号进行运算
指针本身也是一种变量,因此也可以进行运算。但是因为指针变量本身存的是某个其他变量的
地址值,因此该值进行* / %等运算是无意义的。就像两家的门牌号加起来代表不了什么意义。
但是两家门牌号相减就有意义,我们可以知道两家相隔多少间。指针+1,-1是有意义的。+1就
代表指针所指向的格子向后挪一格,-1代表指针所指向的格子向前挪一格。
#include <stdio.h>
int main(void)
{
int a[5] = {555, 444, 333, 222, 111};
int *p;
p = a;
printf("*p = %d.\n", *p);
p += 1;
printf("*p = %d.\n", *p); //444
return 0;
}
#include <stdio.h>
int main(void)
{
int a[5] = {555, 444, 333, 222, 111};
int *p;
p = a;
printf("*p = %d.\n", *p);
p += 2;
printf("*p = %d.\n", *p); //333
return 0;
}
#include <stdio.h>
int main(void)
{
int a[5] = {555, 444, 333, 222, 111};
int *p;
p = a;
printf("*p = %d.\n", *p);
p += 5;
printf("*p = %d.\n", *p); //数组越界访问,得到一个随机数
return 0;
}
#include <stdio.h>
int main(void)
{
int a[5] = {555, 444, 333, 222, 111};
int *p;
p = a;
printf("*p = %d.\n", *p);
printf("*p++ = %d.\n", *p++); //555 ++后置,先运算后+1
return 0;
}
*p++就相当于*(p++),p先与++结合,然后p++整体再与*结合
*p++解析:++先跟p结合,但是因为++后置的时候,本身含义就是先运算后增加1(运算指的是
p++整体与前面的*进行运算,增加1指的是p+1),所以实际上*p++符号整体对外表现的值是*p
的值,运算完成后p再加1。
所以*p++等同于:*p; p+=1;
#include <stdio.h>
int main(void)
{
int a[5] = {555, 444, 333, 222, 111};
int *p;
p = a;
printf("*p = %d.\n", *p);
printf("*p++ = %d.\n", *++p); //444 ++前置,指针先加1,然后再*p取值
return 0;
}
所以*++p等同于: p+=1; *p;
#include <stdio.h>
int main(void)
{
int a[5] = {555, 444, 333, 222, 111};
int *p;
p = a;
printf("*p = %d.\n", *p);
printf("(*p)++ = %d.\n", ++(*p)); // 556 ++前置
return 0;
}
#include <stdio.h>
int main(void)
{
int a[5] = {555, 444, 333, 222, 111};
int *p;
p = a;
printf("*p = %d.\n", *p);
printf("(*p)++ = %d.\n", (*p)++); // 555 ++后置
return 0;
}
总结:++符号和指针结合,总共有以上4种情况。--符号与++符号的情况类似。
8.8 函数传参中使用指针
int add(int a, int b) 函数传参使用了int型数,本身是数值类型。
实际调用该函数时,实参将自己拷贝一份,并将拷贝传递给形参进行运算。实参自己实际是不
参与的。所以,在函数中,是没法改变实参本身的。
#include <stdio.h>
int swap(int a, int b);
int main(void)
{
int x, y;
x = 5;
y = 3;
swap(x, y);
printf("x = %d. y = %d.\n", x ,y); // 5 3
return 0; //实际测试结果:失败,并没有交换。
}
int swap(int a, int b) //交换的不是x和y,交换的只是x和y的一份拷贝
{
int temp;
temp = a; //a是swap内的形参,实际调用时得到的是实参x的一份拷贝,
a = b; //只是和x的值相等而已,其他并无任何关联,因此在这里不能访问到x
b = temp;
return 0;
}
交换失败原因:C语言中,函数调用时,实参传递给形参实际是传值调用。
也就是说,实参x和y将自己的值拷贝一份给形参a和b,在子函数swap中实际交换的
是a和b,而不是实参x和y,因此函数执行完后,x和y的值依然并没有被交换。
#include <stdio.h>
int swap_pointer(int *p1, int *p2);
int main(void)
{
int x, y;
x = 5;
y = 3;
swap_pointer(&x, &y);
printf("x = %d. y = %d.\n", x ,y);
return 0;
}
int swap_pointer(int *p1, int *p2)
{
int temp;
temp = *p1;
*p1 = *p2;
*p2 = temp;
return 0;
}
交互成功:C语言函数调用时,一直都是传值调用。也就是说实际传递的一直都是实参的拷贝
但是本函数中的形参和实参都并不是x和y,而是x和y的地址值。这样,让我们在函数中通过间接访问
*P的方式,在函数内访问到了函数外面调用时的实参。这也是指针存在的意义的体现。
什么是结构体?
结构体是一个集合,集合中包含很多个元素,这些元素的数据类型可以相同,也可以
不相同。所以结构体是一种数据封装的方法。封装才不会太散乱。
结构体存在的意义就在于,把很多数据类型不相同的变量封装在一起,组成一个大的新的数据类型。
被封装起来的那几个变量是有相关性的,用一个结构体封起来,为了做一个区别,便于管理
数据结构:把庞大复杂的数据用一定的方式组织管理起来,便于操作(查找,增加,删除等)这
就叫数据结构。数组就是我们数据结构里面的一种简单组织(里面的成员类型要相同
)复杂一点的有结构体(里面的成员可以不同),更复杂的有链表,哈希表,二叉树,大数据,云计算
去研究二叉树、哈希表还不如研究一下数据库怎么用,那种复杂的数据结构做算法的才会用到
#include <stdio.h>
//1.结构体类型的定义是在函数外面,不是里面
//2.结构体定义的是一个新的组合类型,而不是变量,也消耗内存
// 稍后在定义变量的地方,再使用该结构体类型来定义变量
struct Student
{
char name[20]; //stuedent name
unsigned int num; //student ID
int isMale; //student sex
}; //注意分号不能丢
int main(void)
{
struct Student s1; //s1是一个变量,类型是struct Student
return 0;
}
范例:
#include <stdio.h>
struct Student
{
char name[20]; //stuedent name
unsigned int num; //student ID
int isMale; //student sex
};
int main(void)
{
struct Student s1;
s1.name[0] = 'J';
s1.name[1] = 'i';
s1.name[2] = 'm';
s1.name[3] = '\n';
s1.num = 123;
s1.isMale = 1;
printf("s1.name = %s, s1.num = %d, s1.isMale = %d.\n", \
s1.name, s1.num, s1.isMale);
return 0;
}
范例:
#include <stdio.h>
struct MyStruct
{
int a;
char c;
float f;
double d;
};
int main(void)
{
struct MyStruct s;
s.a = 1444;
s.c = 'k';
s.f = 3.1415;
s.d = 3.423246;
s.a += 100;
printf("s.a = %d.\n", s.a);
printf("s.c = %c.\n", s.c);
return 0;
}
结构体体可以当数组用
struct ArrayStruct
{
int a;
int b;
int c;
int d;
};
int a[4];
结构体的这种定义和数组没有任何区别。要说有什么区别就是结构体定义了类型
数组定义了变量。
ArrayStruct s1;
int a[4];
这种定义就是一模一样的了,都是定义了4个int变量
3. 结构体和数组的关联:数组是一个特殊的结构体,特殊之处在于封装内的各个元素类型是相同的。
其实结构体是数组的一种扩展,把类型相同这个限制给去掉了。仅此而已。
结构体和数组都是对一些子元素的封装,因此定义的时候都是封装作为整体定义,但是使用的时候,都
是使用封装中的子元素。一般结构体变量和数组变量都不会作为一个整体操作。
4.使用结构体的步骤:
第一步:定义结构体类型。结构体类型的定义是在函数外面(函数外面==全局)的
第二步:使用第一步定义类型来定义结构体变量。
第三步:使用变量。实际上使用结构体变量的时候,使用的是结构体变量中封装的各个元素,
而不是结构体变量本身。这一点和数组是一样的。
新增关键字:struct
新增操作符:.
如何使用结构体变量名访问成员
结构体变量名.成员名
printf("%s\n", stu.name); //stu.name等价于&stu.name[0]
4.1 指针访问方式
(1)方式1 (*指针).成员
结构体变量的第一个字节的地址,就是整个结构体变量的指针,
struct student stu = {"zhangsan",'M',12345,96.0};
//将结构体变量的指针(数),写入结构体指针变量pstu.
//结构体变量的指针,是结构体变量第一个字节的地址,结构体变量的
//第一个字节,其实也是结构体变量第一个成员的第一个字节
struct student *pstu = &stu;
(*pstu).gender //(*pstu).gender等价于stu.gender
printf("%s\n", (*pstu).name); //符号点是成员访问符,专门用来访问结构体成员的
printf("%f\n", (*pstu).score);
注意:(*pstu).name,这里必须加括号,让*与pstu相结合,表示先找到结构体变量空间(大房间)
然后再通过.name找到成员name,这个成员就是小房间。
我们之所以要加入()的原因是,.优先级为1,高于*的优先级2,如果我们不加括号的话,
pstu就先和name结合,无法满足我们的要求。 (*指针).成员
(2)方式2 指针->成员
(*pstu).gender
(*指针).成员
事实上这种写法并不好,结构复杂,比较容易写错
printf("%s\n", (*&stu).name); //这种写法没问题,*&这两个符号可以相互抵消
更加人性化的写法
指针->成员 //其实(指针->成员)本质就是这种[(*指针).成员]写法,编译器最终会把
(指针->成员)转化为[(*指针).成员]
(*pstu).gender 改写为 pstu->gender
4.2 传递整个结构体指针
void fun1(struct student *pstu)
{
printf("in fun1, name = %s\n", (*pstu).name);
printf("in fun1, name = %s\n", pstu->name);
}
fun1(&stu);
4.3 传递成员指针
每一个成员变量都有自己的指针
传递数组的时候,传递的本身就是指针
由于传递是指针,子函数通过指针修改数字后,实参也会被修改
如果子函数只需要用到结构体部分成员时,我们可以使用这种传参方式,传递结构体成员指针
不过一般来说,我们更多的是传递整个结构体变量的指针
传递结构体变量成员的指针,与传递普通变量的指针没有区别,只是这些成员变量被包含在了
结构体的内部。
struct student
{
char name[20];
char gender;
unsigned num;
float score;
};
void fun1(char *pname, char *pgender, flaot *pscore)
{
printf("in fun1, name = %s\n", pname);
printf("in fun1, gender = %c\n", *pgender);
printf("in fun1, name = %f\n", *pscore);
}
fun1(stu.name, &stu.gender, &stu.score); //stu.name等价于&stu.name[0]
5. 结构体的初始化
结构体变量和普通变量一样,作为局部变量时,如果定义的时候无初始化也无显示
#include <stdio.h>
struct MyStruct
{
int a;
char c;
float f;
double d;
};
int main(void)
{
struct MyStruct s;
printf("s.a = %d.\n", s.a);
printf("s.c = %c.\n", s.c);
return 0;
}
#include <stdio.h>
struct MyStruct
{
int a;
char c;
float f;
double d;
};
int main(void)
{
struct MyStruct s; //先定义变量,定义同时无初始化
printf("s.a = %d.\n", s.a);
printf("s.c = %c.\n", s.c); //输出的值是随机的
return 0;
}
6. 结构体数组
6.1 声明结构体数组
struct student stu[5];
声明了5个元素的结构体数组,相当于我们一次性声明了5个结构体变量,每一个结构体变量都是struct student
struct student stu[0]
struct student stu[1]
struct student stu[2]
struct student stu[3]
struct student stu[4]
6.2 定义结构体数组(初始化)
定义与声明的区别是定义有初始化
struct student
{
char name[20];
unsigned num;
float score;
};
//完全初始化
struct student stu[5] = {{"zhangsan",111111,60.0},{"lisi",2222222,90.5},{"wangwu",333333,100.0}
{"xiaohua",444444,78.5},{"xiaoming",555555,80.0}};
//部分初始化,顺序初始化前面几个
struct student stu[5] = {{"zhangsan",111111,60.0},{"lisi",2222222,90.5},{"wangwu",333333,100.0}};
//个别初始化,与顺序无关
struct student stu[5] = {[3]={"zhangsan",111111,60.0},[2]={"lisi",2222222,90.5},[4]={"wangwu",333333,100.0}};
6.3 访问结构体数组元素
int i = 0;
int n = sizeof(stu)/sizeof(struct student);
for(i=0; i<n; i++)
{
printf("student %d name = %s\n", i, stu[i].name); //结构体变量用点,指针就用->符号
printf("student %d num = %u\n", i, stu[i].num);
printf("student %d score = %f\n", i, stu[i].score);
}
6.4 传递结构体数组
void fun(int n, struct student *p) //struct student *p 等价于 struct student p[]
{
int i = 0;
for(i=0; i<n; i++)
{
printf("student %d, name = %s\n", i,(p+i)->name ); //得到指针之后,指针加一得到下一个元素的指针
printf("student %d, name = %s\n", i,(p+i)->name ); //指针加一加的是一个元素空间的大小
printf("student %d, name = %s\n", i,(p+i)->name );
}
}
int main(void)
{
fun(5, stu); //stu等价于&stu[0]
}
注意:结构体数组的传参与普通的数组传参没有区别,只不过结构体数组的成员是一个结构体变量而是
9.6 宏定义
#define N 321 //宏定义的格式
宏定义要注意的问题
1. 宏定义一般是在函数的外面
2. 宏定义必须要先定义,再使用宏。如果先使用就会编译报错。
3. 宏定义中宏明一般用大写。不是语法规定的,是一般约定俗成的。
为什么使用宏定义?
在C语言中,一般使用常数的时候,都不是直接使用,而是先把该常数定义为一个宏,然后
在程序中使用该宏名。这样做的好处是,等我们需要修改这个常数时,只需要在宏定义处修改
一次即可。而不是到代码中到处去寻找,看哪里都用过该常数。编译之前原封不动的替换。
#include <stdio.h>
#define N 11111 //严格的时候后面的括号要加上 #define N (11111)
//宏是什么意义,N这个就是宏,一个符号
//符号N就代表了 11111, 11111在这里不是个数字,是一个符号
#define M (10 + N) //宏定义嵌套
#define PI 3.14 //严格的时候后面的括号要加上 #define PI (3.14) 编程老手应该做的事
#define S(r) (PI * r * r) //计算圆面积的宏
#define C(r) (2 * PI * r) //计算圆周长的宏
//#define S(r) (PI * (r) * (r)) 成熟的表现
int main(void)
{
int a;
a = 123;
a = N;
printf("a = %d.\n", a);
return 0;
}
//题目,使用宏定义定义一个宏,表示一年中的秒数
#define SEC_PER_YEAR (365 * 24 * 60 *60) //后面的这个式子要用括号括起来。
//SEC_PER_YEAR要大,名字取的有意义
#define SEC_PER_YEAR (365 * 24 * 60 *60)UL //强制转换成无符号整型,默认是int的,这里已经超出int的范围了
//就会报错
9.7 枚举
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
};
(1)枚举型是一个集合,集合中的元素(枚举成员)是一些命名的整型常量,元素之间用逗号,隔开。
(2)DAY是一个标识符,可以看成这个集合的名字,是一个可选项,即是可有可无的项。
(3)第一个枚举成员的默认值为整型的0,后续枚举成员的值再前一个成员上加1。
(4)可以人为设定枚举成员的值,从而自定义某个范围内的整数。
(5)枚举型是预处理指令#define的替代,看起来比#define定义的宏更加紧凑。
(6)类型定义以分号;结束。
(7)C语言中的枚举变量的取值可以取成员外的整型值,要求不严格,C++是绝对不允许的。
既然枚举也是一种数据类型,那么它和基本数据类型一样也可以对变量进行声明。
方法一: 枚举类型的定义和变量的声明分开
enum DAY
{
MON = 1, TUE, WED, THU, FRI, SAT, SUN
};
enum DAY today; //变量today类型为枚举型enum DAY
方法二:类型定义与变量声明同时进行
enum
{
saturday,
sunday = 0,
monday,
tuesday,
wednesday,
thursday,
friday
}workday; //变量workday的类型为枚举型enum DAY
enum week {Mon=1, Tue, Wed, Thu, Fri, Sat, Sun} days; //变量days的类型为枚举enum week
#include <stdio.h>
enum week{Mon, Tue, Wed, Thur, Fri, Sat, Sun};
int main(void)
{
enum week day;
day = Wed;
printf("day = %d.\n",day); //2
printf("sizeof(enum week) = %d.\n", sizeof(enum week)); //4个字节
return 0;
}
方法三:用typedef关键字将枚举类型定义成别名,并利用该别名进行变量声明:
typedef enum workday
{
saturday,
sunday = 0,
monday,
tuesday,
wednesday,
thursday,
friday
}workday; //此处的workday为枚举型enum workday的别名
workday today, tomorrow; //变量today和tomorrow的类型为枚举型workday,也即enum workday
enum workday中的workday可以省略:
typedef enum
{
saturday,
sunday = 0,
monday,
tuesday,
wednesday,
thursday,
friday
}workday; //此处的workday为枚举型enum workday的别名
workday today, tomorrow; //变量today和tomorrow的类型为枚举型workday,也即enum workday
也可以用这种方式:
typedef enum workday
{
saturday,
sunday = 0,
monday,
tuesday,
wednesday,
thursday,
friday
}; //此处的workday为枚举型enum workday的别名
workday today, tomorrow; //变量today和tomorrow的类型为枚举型workday,也即enum workday
注意:同一个程序中不能定义同名的枚举类型,不同的枚举类型中也不能存在同名的命名常量。
typedef 的用法:
C语言允许用户使用typedef关键字来定义自己习惯的数据类型名称,来替代系统默认的基本类型名称、
数组类型名称、指针类型名称与用户自定义的结构名称、共用体名称、枚举型名称等。
一旦用户在程序中定义了自己的数据类型名称,就可以在该程序中用自己的数据类型名称来定义变量的
类型、数组的类型、指针变量的类型与函数的类型等。
typedef 的4种用法
(1)为基本数据类型定义新的类型名
也就是说,系统默认的所有基本类型都可以利用typedef关键字来重新定义类型名。
typedef unsigned int CONUT;
而且,我们可以使用这种方法来定义与平台无关的类型。比如,要定义一个叫REAL
的浮点类型,在目标平台一上,让它表示最高精度的类型,即:
typedef long double REAL;
在不支持long double 的平台二上,改为:
typedef double REAL;
甚至还可以在连double都不支持的平台三上,改为:
typedef float REAL;
这样,当跨平台移植程序时,我们只需修改一下typedef的定义即可,而不用对其他源码做任何修改。
(2)为自定义数据类型(结构体、共用体和枚举类型)定义简洁的类型名称
typedef struct tagPoint
{
double x;
double y;
double z;
}Point;
在上面的代码中,实际上完成了两个操作:
1. 定义了一个新的结构类型,代码如下:
struct tagPoint
{
double x;
double y;
double z;
};
其中,struct关键字和tagPoint一起构成了这个结构体类型。
2. 使用typedef为这个 新的结构起了一个别名,叫Point,即:
typedef struct tagPoint Point;
因此,现在你可以像int和double那样直接使用Point定义变量,如下面代码:
Point oPoint1 = {100, 100, 0};
Point oPoint2;
3. 为数组定义简洁的类型名称
它的定义方法很简单,与为基本数据类型定义新的别名方法一样,
typedef int IN_ARRAY_100[100]
4. 为指针定义简洁的名称
typedef char* PCHAR;
C语言进阶篇
1. 数据类型
1.1 基本数据类型
数据类型分2类:基本数据类型+符合类型
基本类型:char short int long 前4种是整型 float double 后两种是浮点型
复合类型:数组 结构体(数组进化成结构体) 共用体 类(C语言没有类C++有,类其实就是结构体的进一步进化)
1.1.1 内存占用与sizeof运算符
sizeof用来测当前数据类型在当前数据平台上占用几个字节
数据类型就好像一个一个的模子,这些模子刻出来的东西我们叫变量。
变量存储在内存中,需要占用一定的内存空间。一个变量占用多少空间是由
变量的数据类型决定的。
每种数据类型,在不同的机器平台上占用内存是不同的。我们一般讲的时候都是以32
位CPU为默认硬件平台描述的:
char 1字节
short 2字节
int 4字节
long 4字节
float 4字节
double 8字节
#include <stdio.h>
int main(void)
{
printf("sizeof(char) = %d.\n", sizeof(char)); //1
printf("sizeof(short) = %d.\n", sizeof(short)); //2
printf("sizeof(int) = %d.\n", sizeof(int)); //4
printf("sizeof(long) = %d.\n", sizeof(long)); //4
printf("sizeof(float) = %d.\n", sizeof(float)); //4
printf("sizeof(double) = %d.\n", sizeof(double)); //8
return 0;
}
1.1.2 有符号数和无符号数
对于char short int long等整型类型的数,都分有符号和无符号数
而对于float和double这种浮点类型来说 ,只有有符号数,没有无符号数
对于C语言来说,数(也就是变量)是存储在内存中一个一个的格子中的,
存储的时候是以二进制方式存储的。对于有符号数和无符号数来说,存储方式
不同的。譬如对于int来说unsigned int无符号数,32位(4字节)全部用来存
数的内容所以表示的数的范围是0~4294967295(2^32-1)
signed int 有符号数,32位中最高位用来存符号(0表示正数,1表示负数),剩余的31
位用来存数据。所以可以表示的数的范围是-2147483648(2^31) ~ 2147483647(2^31-1)
结论:从绝对数值来说,无符号数所表示的范围要大一些。因为有符号数使用1个二进制位来
表示正负号。
对于char short long的换算方法就是把对应的二进制一换就行了。
对于float和double这种浮点类型的数,它在内存中的存储方式和整型数不一样。类似于科学计数法。
例如213 表示为2.13*10^2
所以float和int相比,虽然都是4字节,但是在内存中存储的方式完全不同。所以同一个4字节的内存,
如果存储时是按照int存放的,取的时候一定要按照int型方式去取。如果存的时候和取的时候理解的
方式不同,那数据就完全错了。
#include <stdio.h>
int main(void)
{
int a;
a = 123;
printf("a = %d, a = %f.\n", a, a); //a = 123, a = 0.000000
return 0;
}
解释:因为printf打印时,%d表示打印的是一个整型数,%f表示打印的是一个浮点数。
我们实际传递a过去的时候,a是int型的,所以相当于我们是以int型存储规则来存储的。
然后打印的时候按照%f的方式去解析,实际相当于告诉编译器这个格子里存的是一个浮点数,
于是编译器就按照浮点数的存储规则去取。所以取出来的和存进去的会不同。
备注:详细的数制存储可以查找资料:计算机原码、反码、补码知识。
总结:存取方式上主要有两种,一种是整型一种是浮点型,这两种存取方式完全不同,
没有任何关联,所以是绝对不能随意改变一个变量的存取方式。
在整型和浮点型之内,譬如说4中整型char、short、int 、long只是范围大小不同而已,
存储方式是一模一样的。float和double存储原理是相同的,方式上有差异,导致了 能
表示的浮点型的范围和精度不同。
1.2 空类型(关键字void)
C语言中的void类型,代表任意类型,而不是空的意思。任意类型的意思不是说想变成谁就变成谁,
而是说它的类型是未知的,是还没指定的。(就像一张还没有填写的支票,填10块10块到手,填100
万100万到手了。)不是空,而是暂时没有指定。
void * 是void类型的指针。void类型的指针的含义是:这是一个指针变量,该指针指向一个void
类型的数(或变量),void类型的数就是说这个 数有可能是int,也有可能是float,也有可能是个
结构体,哪种类型都有可能,只是我当前不知道。
void型指针的作用就是,程序不知道那个变量的类型,但是程序员自己心里知道。程序员如何知道?
当时给这个变量赋值的时候是什么类型,现在取的时候就还是什么类型。这些类型对不对,能否兼容,
完全由程序员自己负责。编译器看到void就没办法帮你做类型检查。
在函数的参数列表和返回值中,void代表的含义是:
一个函数形参列表为void,表示这个函数调用时不需要(注意:这里不是说不能,
你可以做到,但是你没必要去做,不能是你没有这个能力去做)给它传参。(就像我今天带饭了,
中午就不需要请我吃饭了,不是说不能,也可以请的。请桌子吃饭那叫不能)
#include <stdio.h>
void test(void);
int main(void)
{
test(); //调用函数时不需要传参的
return 0;
}
//void类型参数和返回值,表示不需要
void test(void)
{
printf("I am a void test.\n");
}
#include <stdio.h>
void test(void);
int main(void)
{
test(12); //也可以传参,对这个函数没有什么影响,只不过这边编译器帮我们阻止了
return 0; //实际上所有的C语言在执行的时候,所有的函数都是有传参的,所有的函数都是有返回值的
} //这是C语言在机器里面实现所决定的,你把它写成void它传参还在,只是不去管它而已。
void test(void)
{
printf("I am a void test.\n");
}
返回值类型是void,表示这个函数不会返回一个有意义的返回值。所以调用者也不要想着去
#include <stdio.h>
void test(void);
int main(void)
{
int a = 444;
void *pVoid;
pVoid = &a;
printf("*pVoid = %d.\n", *(int *)pVoid); //因为指针变量刚开始没有指定类型,引用之前要先强制转换成想要的类型
//就是告诉编译器这个pVoid指针我现在把它当做int类型的指针来用,
return 0; //按照int类型来取值,取出来的值就是444
}
#include <stdio.h>
void test(void);
int main(void)
{
int a = 444;
void *pVoid;
pVoid = &a;
printf("*pVoid = %d.\n", *(float *)pVoid); //程序员要按照float类型来取,编译器是不知道的,程序员在这里欺骗了
//编译器,编译在这里选择相信你了,结果取出来的值是错的*pVoid = 1834341848.
//这个错误是程序员自己造成的。别人存明明是int的程序员去取的时候非要告诉编译器
return 0; //是float的
}
注意:void *类型的指针,可以指向任意类型的数。但是程序员使用的时候心里必须非常清楚,
存放是和取时的类型必须相同,如果不相同,编译器是没法发现的,结果都要程序员自己负责。
从这里可以看出来,我们在写C语言程序的时候,程序员说了算,而不是编译器,C语言这种语言其实很将礼貌的,它自己的权利是
很小的。这是C语言的一个设计原则。
C语言设计的基本理念:
C语言相信程序员永远是对的,C语言相信程序员都是高手,C语言赋予了程序员最大的权利。
编译器是可以选择相信你的。上面的例子就是一个很好的一个例子,还有个典型例子就是数组的越界访问,可以访问,只不过访问到的值不对而已。
C语言是不做检查的,C语言就相当于一把尖刀,要是拿在好人手里干的就是好事,要是拿在坏人手里干的就是坏事。
C语言是善恶不分的,它把权利全部赋予给了程序员。程序员想干啥就干啥。所以C语言的程序员必须自己对程序的对错负责,
必须随时脑袋清楚,知道自己在干嘛。你要是脑袋不清楚还想编译器帮你做检查那你就是想多了。你要是像其他的高级语言如Java
程序本身会帮你做很多检查,编译器会帮做很多事情,你自己不用做,这就是高级语言和C这种中下层语言最大的差别。C语言是比较难的,
Java这种高级语言是比较简单的,因为C这种语言你稍微不注意就会犯错了。因为C赋予你的权利太大了,就像你拿着一把枪,连保险没上,
拿出来想突突谁就突突谁,这个是很危险的,稍微不注意就会犯错。像Java C#这种语言连内存都帮你检查了,想犯错误都难,它对你监管太
严格了。
1.3 数据类型转换
为什么需要数据类型转换: C语言中有各种数据类型,写程序时需要定义各种类型的变量。
这些变量需要参与运算。C语言有一个基本要求就是:不同类型的变量是不能直接运算的。
千万不要以为1(int)+1.5(float)=2.5是天然的事情。
也就是说,int和float类型的变量不能直接加减等运算。你要运算,必须先把两种类型转成
相同的类型才可以。
#include <stdio.h>
void test(void);
int main(void)
{
int a = 3;
float b = 3.5;
printf("a + b = %f.\n", a + b); //6.500000结果正确
return 0;
}
分析:
1. 编译器发现a+b中a和b类型不同。这时候两个要加,编译器会进行隐式类型转换把两个转成类型相同。
根据隐式类型转换的规则,编译器构造了一个临时变量(譬如叫float f1),其实是没有名字,然后
把f1赋值为a转成float类型的值,之后参与运算时是用f1去和b相加的。a的类型是没有被改变的。
加完之后得到一个临时变量(譬如叫float f2),也没有名字,这个临时变量再参与之后的运算。
题目中printf("a + b = %f.\n", a + b); printf中是%f,所以需要一个float类型的变量来打印。
于是乎,f2直接拿去打印显示。得到结果6.500000
#include <stdio.h>
void test(void);
int main(void)
{
int a = 3;
float b = 3.5;
printf("a + b = %d.\n", a + b); //a + b = 706376120结果错误
//float类型f2被当做int解析了,所以错了
return 0;
}
#include <stdio.h>
void test(void);
int main(void)
{
int a = 3;
float b = 3.5;
printf("a + b = %d.\n", (int)(a + b)); //6 结果正确
//这里f2 被强制转换成了int类型
return 0;
}
#include <stdio.h>
void test(void);
int main(void)
{
int a = 3;
float b = 3.5;
int c;
c = a + b;
printf("c = %d.\n", c); //结果是 6
return 0;
}
分析:
1. 编译器发现a和b类型不同,于是乎隐式类型转换,将a转成float类型临时变量f1
2. f1 + b,得到一个临时变量f2,值为float类型的6.500000
3. c = f2 编译器发现c和f2的类型不相同,于是乎隐式类型转换,注意:这是在赋值运算
不是去改变左值的类型,而是把右值转换成左边目标类型(左边是老大,右边是赊账的,左边是
收账的),于是编译器隐式类型转换,将f2转成临时变量int型的i(随便取的名字),值为6.
4. printf打印时发现%d,右边的变量C类型为int,编译器检查发现类型匹配,直接打印。
1.3.1 隐式转换
隐式转换就是自动转换,是C语言默认会进行的,不用程序员干涉。
C语言的理念:隐式类型转换默认朝精度更高、范围更大的方向转换。
隐式类型转换是不会出错的,但是有可能会降低精度(但是不是出错)。
1.3.2 强制类型转换
C语言默认不会这么做,但是程序员我想这么做,所以我强制这么做了。
强制类型转换有可能出错,需要程序员自己来负责。
总结:数据类型其实就是表明了数据在内存里面以什么样的方式存和取的,它其实是一种规则。
比如说int是一种规则,按照这种方式去取或存。存和取的方式必须是一样的。
1.4 C语言与bool类型
C语言中原生类型没有bool,C++中有。
在C语言中如果需要使用bool类型,可以用int来代替,就是有点浪费而已,但是没有办法。
很多代码体系中,用以下宏定义来定义真和假
#define TRUE 1
#define FALSE 0
#include <stdio.h>
//在bool类型的世界,除了0是假之外,其余数都是1(真)
int main(void)
{
int a;
a = -23;
if (a) //真
{
printf("a = %d.\n", a);
}
return 0;
}
#include <stdio.h>
int main(void)
{
float a;
a = -23.00;
if (a) //真
{
printf("a = %f.\n", a);
}
return 0;
}
2. 变量和常量
2.1 变量
变量,指的是程序运行过程中,可以通过代码使它的值改变的量。
2.1.1 局部变量
定义再函数中的变量,就叫局部变量
2.1.2 全局变量
定义再函数外面的变量,就叫全局变量
#include <stdio.h>
int g_a; //g_a定义在函数外面,因此是全局变量
int main(void)
{
int a; //a定义在main函数中,所以是局部变量
return 0;
}
局部变量和全局变量的对比:
(1)定义同时没有初始化,则局部变量的值是随机的,而全局变量的值是默认为0。
(2)使用范围上:全局变量具有文件作用域(你把整个文件想象成一个很大的括号就完了),而局部变量只有代码块作用域。
(3)生命周期上:全局变量是在程序开始运行之前的初始化阶段就诞生(因为在main函数之前),到整个程序结束退出的时候才
死亡;而局部变量在进入局部变量所在代码块时诞生,在该代码块退出的时候死亡。
(4)变量的分配位置:全局变量分配在数据段上,而局部变量分配在栈上。
判断一个变量能不能使用,有没有定义,必须注意两点:
第一,该变量定义的作用域是否在当前有效,是否包含当前位置;
第二,变量必须先定义后使用。所以变量引用一定要在变量定义之后。(函数的声明也有这个规律)
基本概念:
作用域:起作用的区域,也就是可以工作的范围。
代码块:所谓代码块,就是用{}括起来的一段代码。
数据段:数据段存的是数,像全局变量就是存在数据段的
代码段:存的是程序代码,一般是只读的,
栈(stack):先进后出。C语言中局部变量就分配在栈中。变量出栈就死亡了。
#include <stdio.h>
int g_a; //准确的应该这么说,全局变量g_a的作用域应该在这个文件中,定义的这行int g_a之后
int main(void)
{
int a; //a is defined inside the function, called local variable
printf("a = %d, g_a = %d.\n", a, g_a);
return 0;
}
结果:
局部变量a的值是随机的,全局变量g_a的值是0
结论:在不初始化的前提下,局部变量的值是随机的,全局变量的值是0
int func(void)
{
int i;
i = a; //error: 'a' undeclared (first use in this function) 提示a没有定义
//a是main函数中定义的局部变量,它的作用域只有main函数中的代码块{}
return i; //因此在这里不能访问
}
#include <stdio.h>
int g_a;
int main(void)
{
int i;
for (i=0; i<10; i++)
{
int b = 5; //b定义在这里,则作用域只有for后面的{}里面,外面是看不见的
b++;
}
printf("b = %d.\n", b); //error: 'b' undeclared (first use in this function)
return 0;
}
#include <stdio.h>
int func(void);
int g_a = 1;
int main(void)
{
func();
return 0;
}
int func(void)
{
printf("g_a = %d.\n", g_a); //调用全局变量g_a没有问题
return 0;
}
#include <stdio.h>
void func(void);
int g_a;
//全局变量的特点:在整个文件中所有的函数内都可以访问全局变量,而且访问的都是
//该全局变量本身。如果在你之前某个函数中更改了它的值,则后面再引用时它的值是
//前面那次更改之后的值。
int main(void)
{
printf("g_a = %d.\n", g_a); //0
func();
printf("g_a = %d.\n", g_a); //100
g_a += 1;
printf("g_a = %d.\n", g_a); //101
return 0;
}
void func(void)
{
g_a = 100;
}
#include <stdio.h>
void func1(void);
int main(void)
{
func1();
i = 5; //error: 'i' undeclared (first use in this function)
//因为i是定义在函数func中的局部变量,所以i的作用域为代码块作用域,所以i
return 0; //只在func1函数内部有效,在funcz外面是不能访问i的。所以这里i会无定义。
}
void func1(void)
{
int i = 1;
i++;
printf("i = %d.\n");
}
#include <stdio.h>
int g_i = 13;
int main(void)
{
printf("g_i = %d.\n", g_i); //输出13
return 0;
}
实验结论:
首先,main函数是一个程序运行最开始执行的东西,所有的其他函数都只能在main函数中
被直接或者间接的调用才能被执行。main函数的执行其实就是整个程序的生命周期,main
函数--return返回,整个程序就结束了。
其次,全局变量的定义和初始化是在main函数运行之前发生的。
2.1.1.1 普通局部变量(auto)
普通的局部变量定义是直接定义,或者在定义前加auto关键字
#include <stdio.h>
int main(void)
{
auto int i = 1; //自动局部变量,其实就是普通局部变量,auto是可以省略的
return 0;
}
#include <stdio.h>
void func1(void);
int main(void)
{
func1(); //每次进入函数的i不是同一个i
func1(); //因为上一次函数调用结束,i也会伴随死亡
func1();
// i = 5;
return 0;
}
void func1(void)
{
int i = 1; //普通的局部变量
i++;
printf("i = %d.\n", i); //输出i = 2 i = 2 i = 2
}
局部变量i的解析:
在连续三次调用func1中,每次调用时,在进入函数func1后都会创造一个新的变量i,并且给它
赋初值1,然后i++时加到2,然后printf输出时输出2。然后func1本次调用结束,结束时同时杀死
本次创造的这个i。这就是局部变量i的整个生命周期。
下次再调用该函数func1时,又会重新创造一个i,经历整个程序运算,最终在函数运行完退出时再次被杀死。
就像家里的电饭锅,用完了得洗刷,方便下次继续用
#include <stdio.h>
void func1(void);
void func_static(void);
int main(void)
{
func1(); //2
func1(); //2
func1(); //2
func_static(); //a = 2
func_static(); //a = 3
func_static(); //a = 4 静态局部变量的值是累积的
return 0;
}
void func1(void)
{
int i = 1; //每次进入时是重新创造了一个i,并且给它赋初值1
i++;
printf("i = %d.\n", i); //每次func1退出时i就被杀死了
}
void func_static(void)
{
static int a = 1; //静态局部变量
a++;
printf("a = %d.\n", a);
}
#include <stdio.h>
void func_global(void);
int g_a = 1;
int main(void)
{
func_global(); //a = 4
func_global(); //a = 7
func_global(); //a = 10 全局变量的值会累积
return 0;
}
void func_global(void)
{
g_a += 3;
printf("g_a = %d.\n", g_a);
}
2.1.1.2 静态局部变量(static)
总结:
1. 静态局部变量和普通局部变量不同。静态局部变量也是定义在函数内部的。
静态局部变量定义时前面要加static关键字来标识,静态局部变量所在的函数
在调用多次时,只有第一次才经历变量定义和初始化,以后多次调用时不再定义和初始化,
而是维持之前上一次调用时执行后这个变量的值。本次接着来使用。
2. 静态局部变量在第一次函数被调用时创造并初始化,但在函数退出时它不死亡,而是保持其
值等待函数下一次调用。下次调用时不再重新创造和初始化该变量,而是直接用上一次留下的值
为基础来进行操作。
3.静态局部变量的这种特性,和全局变量非常类似。它们的相同点是都创造和初始化一次,以后
调用时值保持上次的不变(长命)。不同点是作用域不同。
2.1.1.3 跨文件引用全局变量(extern)
就是说,你在一个程序的多个.c源文件中,可以在一个.c文件中定义全局变量g_a,并且可以在别的
另一个.c文件中引用该变量g_a(引用前要声明)
函数和全局变量在C语言中可以跨文件引用,也就是说他们的链接范围是全局的,具有文件链接属性,
总之意思就是全局变量和函数是可以跨文件看到的(直接影响就是,我在a.c和b.c中各自定义了一个
函数func,名字相同但是内容不同,编译报错)
那么C语言怎么保证自己负责的模块的全局变量和函数不与别人的命名一样?
方法就使用静态全局变量,静态函数
关于函数名前面加static的一些解释:
在函数的返回类型前加上关键字static,函数就被定义成为静态函数。函数的定义和声明默认情况下是
extern的,但是静态函数只是在声明它的文件当中可见,不能被其他文件所用。定义静态函数的好处:
(1)其他文件中可以定义相同名字的函数,不会发生冲突。
(2)静态函数只能在声明它的文件中可见,其他文件不能引用该函数。
2.1.1.4 register关键字
register(寄存器),C语言的一个关键字
#include <stdio.h>
void func_register(void);
int main(void)
{
func_register(); //5
func_register(); //5
func_register(); //5
return 0;
}
void func_register(void)
{
register int i = 1;
i += 4;
printf("i = %d.\n", i);
}
总结:register类型的局部变量表现上和auto是一样的,这东西基本没用,知道就可以了。
register被称为C语言中最快的变量。C语言的运行时环境承诺,会尽量将register类型的
变量放到寄存器中去运行(普通的变量是在内存中),所以register类型的变量访问速度会
快很多。但是它是有限制的;首先寄存器数目是有限的,所以register类型的变量不能太多;
其次register类型变量在数据类型上有限制,譬如你就
2.1.2 全局变量
定义在函数外面的变量,就叫全局变量
2.1.2.1 普通全局变量
普通全局变量就是平时使用的,定义前不加任何修饰词。普通全局变量可以在各个文件中使用,
前提是要声明一下,可以在项目内别的.c文件中被看到,所以要确保不能重名。
2.1.2.2 静态全局变量
静态全局变量就是用来解决重名问题的。静态全局变量定义时在定义前加static关键字,
告诉编译器这个变量只在当前本文件内使用,在别的文件中绝对不会使用。这样就不用担心重名问题了。
所以静态的全局变量就用来在我定义这个全局变量并不是为了给别的文件使用,本来就是给我这文件自己
使用的。
2.2 常量
常量,程序运行过程中不会改变的量。常量的值在程序运行之前初始化的时候给定一次,
以后都不会变了,以后一直是这个值。会把这个变量丢到寄存器里面去,不在内存里面。
这样的话CPU的访问的速度就变快了。C语言的运行环境承诺,会尽量将register类型的变量
放到寄存器中去运行(普通的变量是在内存中),所以regester类型的变量访问速度会快很多。
但是它是有限制的,首先寄存器数目是有限的,所以register类型的变量不能太多;其次regester
类型变量在数据类型上有限制,譬如你就不能定义double类型的register变量。一般只是在内核或者
启动代码中,需要反复使用同一个变量这种情况下才会使用register类型变量。
2.2.1 #define定义的常量
#define N 20 //符号常量
int a[N];
#include <stdio.h>
#define N 20
int main(void)
{
int a[N];
N = 12; //error: lvalue required as left operand of assignment
//不能给常量赋值
printf("sizeof(N) = %ld.\n", sizeof(a)); //20*4 = 80
return 0;
}
2.2. const关键字
const int i = 14; //只能有一次机会,以后就不会改了
#include <stdio.h>
int main(void)
{
const int i = 10;
i = 21; //error: assignment of read-only variable 'i'
//不能给常量赋值
return 0;
}
const和指针结合,共有4种形式
(1)const关键字,在C语言中用来修饰变量,表示这个变量是常量。既然是用来修饰变量,当然可以用来修饰指针,
因为指针本来就是变量的一种。
(2)const修饰指针有4种形式,区分清楚这4种即可全部理解const和指针。
第一种:const int *p;
第二种:int const *p;
第三种:int * const p;
第四种:const int * const p;
(3)关于指针变量的理解,主要涉及到2个变量:第一个是指针变量p本身,
第二个是指p指向的那个变量(*p)。一个const关键字只能修饰一个变量,所以弄清楚这4个表达式的关键就是搞清楚
const放在某个位置是修饰谁的。
#include <stdio.h>
int main(void)
{
int a;
//第一种
const int *p1; //p1本身不是const的,而p1指向的变量是const的
*p1 = 4; //error: assignment of read-only location '*p1' 说明*p1是个常量
p1 = &a; //没有报错,说明p1是个变量
//第二种
int const *p2; //p2本身不是const的,而p指向的变量是const的
*p2 = 5; //error: assignment of read-only location '*p2' 说明*p2也是个常量
p2 = &a; //没有报错,说明p2是个变量
//第三种
int * const p3; //p3本身是const的,p指向的变量不是const的
*p3 = 6; //编译没有报错,说明*p不是常量
p3 = 6; //assignment of read-only variable 'p3',说明p3是个常量,意思就是初始化完成后就不能改了
//第四种
const int * const p4; //p本身是const的,p指向的变量也是const的
p4 = &a; //error: assignment of read-only variable 'p4' 说明p4为常量
*p4 = 5; //error: assignment of read-only location '*p4' 说明*p4为常量
return 0;
}
总结:记忆方法为const往右看,先遇到p说明是修饰p的,如果中间隔*就是修饰*p的
const型指针有什么用?
char *strcpy(cosnt char *dst, const char *src);
字符串处理函数strcpy,它的函数功能是把src指向的字符串,拷贝到dst中。
src所指向的的字符串是一个字符串常量,改不了
2.2.3 枚举常量
枚举常量是宏定义的一种替代品,在某些情况下会比宏定义好用。
在有限种的情况下用,比如描述星期几,只有七种情况
枚举就是把所有的可能全部列举出来(可以列举完的)
有的情况就没法枚举,比如食品上细菌的数量
#include <stdio.h>
enum Color
{
GREEN = 1,
RED,
BLUE,
GREEN_RED = 10,
GREEN_BLUE
}ColorVal;
int main(void)
{
printf("sizeof(ColorVal) = %ld.\n", sizeof(ColorVal)); //4字节
return 0;
}
enum只是定义了一个常量的集合。
2.2.4 枚举与#define宏的区别
(1)#define宏常量是在预编译阶段进行简单替换。枚举常量则是在编译的时候确定 其值。
(2)一般在编译器里,可以调试枚举常量,但是不能调试宏常量。
(3)枚举可以一次定义大量相关的常量,而#define宏一次只能定义一个。
一般的定义方式如下:
enum enum_type_name
{
ENUM_CONST_1,
ENUM_CONST_2,
...
ENUM_CONST_n
}enum_variable_name;
注意:enum_type_name是自定义的一种数据类型名,而enum_variable_name为enum_type_name
类型的一个变量,也就是我们平时常说的枚举变量。实际上enum_type_name类型是对一个变量
取值范围的限定,而花括号内是它的取值范围,enum_type_name类型的变量enum_variable_name
只能取值为花括号内的任何一个值,如果赋给该类型变量的值不在列表中,则会报错或警告。
ENUM_CONST_1,ENUM_CONST_2,...,ENUM_CONST_n这些成员都是常量,也就是我们平时所说的的枚举
常量(常量一般用大写)。enum变量类型还可以给其中的常量符号赋值,如果不赋值则会从被赋
初值的那个常量开始依次加1,如果都没有赋值,它们的值从0开始依次递增1.
3. 多文件C语言项目
3.1 简单的C语言程序(项目)只有一个C文件(a.c),编译的时候gcc a.c -o a
3.2 复杂的C语言程序(项目)是由多个C文件构成的。譬如一个项目中包含2个C文件(a.c, b.c)
,编译的时候gcc a.c b.c -o ab,执行的时候./ab
实验:
在a.c和b.c中分别定义main函数,各自单独编译时没有问题;但是两个文件作为一个项目来编译
gcc a.c b.c -o ab的时候,就会报错。multiple definition of `main'
为什么报错?
因为a.c和b.c这时候组成了一个程序,而一个程序必须有且只有一个main函数。
3.3 为什么需要多文件项目?为什么不在一个.c文件中写完所有的功能?
因为一个真正的C语言项目是很复杂的 ,包含很多个函数,写在一个文件中不利于查找、组织、
识别,所以人为的将复杂项目中的很多函数,分成一个一个的功能模块,然后分开在不同的.c
文件中,于是乎有了多文件项目。
所以,在b.c中定义的一个函数,很可能a.c中就会需要调用。你在任何一个文件中定义的任何
一个函数,都有可能被其他任何一个文件中的函数来调用。但是大家最终都是被main函数调用的,
有可能是直接调用,也可能是间接调用(意思是main函数里面并没有直接去使用它,而是main函数
中的使用的函数去调用了它)。
3.4 多文件项目中,跨文件调用函数
在调用函数前,要先声明该被调用函数的原型。只要在调用前声明了该函数,那么调用时就好像这个
函数是定义在本文件中的函数一样。
在一个目录下有a.c b.c两个源文件
a.c的源代码为:
#include <stdio.h>
void func_in_b(void);
void func_in_a(void);
int main(void)
{
printf("I am main in a.c.\n");
func_in_b(); //direct call
func_in_a(); //indirect call
return 0;
}
void func_in_a(void)
{
func_in_b();
}
b.c的源代码为:
#include <stdio.h>
void func_in_b(void)
{
printf("I am main in b.c.\n");
}
注意:main函数不论是在a.c还是b.c都是一样的,并不是说main函数在a.c里面a.c就与众不同
a.c的源代码为:
#include <stdio.h>
void func_in_b(void); //这里函数func_in_a调用了func_in_b,因此也要声明
void func_in_a(void)
{
func_in_b();
}
b.c的源代码为:
#include <stdio.h>
void func_in_b(void);
void func_in_a(void);
int main(void)
{
printf("I am in in b.c.\n");
// func_in_b(); //direct call
func_in_a(); //indirect call
return 0;
}
void func_in_b(void)
{
printf("I am in in b.c.\n");
}
总结:函数使用的三大要素:函数定义、函数声明、函数调用
1.如果没有定义,只有声明和调用:编译时会报连接错误。undefined referece to ‘’
2.如果没有声明,只有定义和调用:编译时一般会报警告,极少数 情况下不会报警告。
但是最好加上声明。
3. 如果没有调用,只有定义和声明:编译时一般会报警告(有一个函数没有使用),有
时不会报警告。这时候程序执行不会出错,只是你白白的写了几个函数,而没有使用浪费了而已。
实验:在一个项目的两个.c文件中,分别定义一个名字相同的函数,结果?
编译报错multiple definition of ‘func_in_a’
结论:在一个程序中,不管是一个文件内,还是该程序的多个文件内,都不能出现函数名
重复的情况,一旦重复,编译器就会报错。主要是因为编译器不知道你调用改函数时到底
是哪个函数,编译器在调用时是根据函数名来识别不同的函数的。
3.5 跨文件调用变量
(1)通过实验验证得出结论,在a.c中定义的全局变量,在a.c中可以使用,在b.c中不可以直接使用
,编译时报错erro:'g_a' undeclared (first use in this function)
(2)想在b.c中使用a.c中定义的全局变量,有一个间接的使用方式。在a.c中写一个函数,然后函数中
使用a.c中定义的该全局变量,然后在b.c中先声明函数,在使用函数。即可达到在b.c中间接引用a.c中
变量的目的。
a.c的源码
#include <stdio.h>
void func_in_b(void);
int g_a = 12;
void func_in_a(void)
{
g_a = 24;
printf("I am in func_in_a of a.c, g_a = %d.\n", g_a);
func_in_b();
}
b.c的源码
#include <stdio.h>
void func_in_b(void);
void func_in_a(void);
int main(void)
{
printf("I am in in b.c.\n");
// printf("I am main in a.c, g_a = %d.\n", g_a);
// func_in_b(); //direct call
func_in_a(); //indirect call
return 0;
}
void func_in_b(void)
{
printf("I am in in b.c.\n");
}
这种使用方法太麻烦了
(3)想在b.c中直接引用a.c中定义的全局变量g_a,则必须在b.c中引用前先声明g_a,如何声明变量?
extern int g_a;
b.c的源码
#include <stdio.h>
void func_in_b(void);
void func_in_a(void);
extern int g_a; //声明变量
int main(void)
{
printf("I am in in b.c.\n");
printf("I am main in a.c, g_a = %d.\n", g_a);
// func_in_b(); //direct call
// func_in_a(); //indirect call
return 0;
}
void func_in_b(void)
{
printf("I am in in b.c.\n");
}
a.c的源码
#include <stdio.h>
void func_in_b(void);
int g_a = 12; //定义变量
void func_in_a(void)
{
g_a = 24;
printf("I am in func_in_a of a.c, g_a = %d.\n", g_a);
func_in_b();
}
extern关键字
extern int g a;这句话是一个全局变量g_a的声明,这句话告诉编译器,我在外部(程序中不是本文件的
另一个文件)某个地方定义了一个全局变量int g_a,而且我现在要在这里引用它告诉你编译器一声,不用
报错了。
问题:
1. 我只在b.c中声明变量,但是别的文件中根本就没定义这个变量,会怎么样?
答案是编译器报错(链接错误)undefined to 'g_b'
2. 我在a.c中定义了全局变量g_a,但是b.c中没有声明g_a,引用该变量会怎么样?
答案是直接报错了,未定义。函数在引用前也应该先声明。
3. 在a.c中定义,在b.c中声明,a.c和b.c中都没有引用该变量,会怎么样?
答案是不会出错,只是白白的定义了一个变量没用,浪费了。
结论:不管是函数还是变量,都有定义、声明、引用三要素。其中,定义是创造这个变量或者函数,声明
是向编译器交代它的原型。引用时使用这个变量或函数。如果没有定义只有声明和引用,编译时一定会报错。
undefined referece to 'xxx'
在一个程序里面,一个函数可以定义一次,引用可以有无数次,声明可以有无数次。因为函数定义或者变量的
定义实际上是创造了这个函数/变量,所以只能有一次。(多次创造同名的变量会造成变量名重复,冲突;多次
创造同名的函数也会造成函数名重复冲突)。声明是告诉编译器变量/函数的原型,在每个引用了这个全局变量
/函数的文件之前都要声明该变量/函数,a.c使用
了a.c里面就要声明,b.c里面使用了b.c里面就要声明,但是在一个文件里面声明一次就可以了,
声明是
局部变量能否跨文件使用?
不可以,因为局部变量属于代码块作用域。他的作用域只有他定义的那个函数内部。就算是同文件的其他函数都
不行,别说是跨文件了。
静态局部变量能不能跨文件使用?
不能。因为本质上还是个局部变量。
讨论跨文件使用问题,只用讨论全局变量和函数就可以了。
函数有局部函数一说吗?
没有,只有全局函数,C语言函数里面是不能嵌套定义函数,你可以引用函数。
3.6 头文件的引入
3.6.1 为什么需要头文件?
从之前可以看到,函数的声明是很重要的。当我们在一个庞大的项目中,有很多个源文件,
每一个源文件中都有很多个函数,并且需要在各个文件中相互穿插引用函数。这时候声明函数
就得把你累死,是一件纯体力活。这时候头文件就派上用场了。你把所有的声明都写在头文件里面
去,然后在你需要使用这些头文件的时候,去包含它就行了。
文件包含的意思就是把这个文件的内容在包含的地方原地展开
用来收集函数和全局变量的声明的,主要是收集函数声明
b.c的源码
#include <stdio.h>
#include "a.h"
/*
//在没有头文件时,需要使用别的.c文件中定义的函数时,都要
//先在本文件中先去声明该函数的原型 ,否则编译器要叫
//所以很麻烦
int add(int a, int b);
int sub(int a, int b);
*/
int main(void)
{
int a = 23, b = 43;
printf("a + b = %d.\n", add(a, b));
return 0;
}
a.c的源码
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
a.h的源码
int add(int a, int b);
int sub(int a, int b);
3.6.2 #include包含 头文件时,用<>和""的区别
<>用来包含系统自带的头文件,系统自带指的是不是你写的,是编译器或者库函数或者
操作系统提供的头文件。比如stdio.h就是编译器提供的,它是一个库函数的头文件,
stdio.h的位置在/usr/include/下面
""用来包含项目目录中的头文件,这些一般是我们自己写的。
stdio.h也可以用""来包含"stdio.h",那么编译器就先在当前目录下找,如果找到了就算
数,如果找不到就在系统目录下找,不过最好不要这么写,别人还以为这个头文件这是你写的呢
自己写的头文件一般都会加上类似这么几行话
#ifndef __A_H__
#define __A_H__
int add(int a, int b);
int sub(int a, int b);
#endif
这是为了防止重复包含头文件,
不要在头文件里面去定义变量,因为这时该头文件被多个源文件包含时,就会出现重复定义问题。全局变量
的定义就应该放在某个源文件中,然后在别的源文件中使用前extern声明。
5 条件编译
5.1 为什么要条件编译
5.1.1 什么是条件编译
对程序源代码的各部分有选择地进行编译
5.1.2 为什么要条件编译
(1)提高程序的适应性,减少目标代码的体积
(2)使用条件编译制作程序的不同客户版本
(3)使程序移植更加方便
5.2 常用的条件编译命令
#if #else #endif
#if #elif #endif
#ifdef #ifndef
#if defined #if !defined
14.1 文件的基本概念

2万+

被折叠的 条评论
为什么被折叠?



