1. Hello World
编写C语言代码:hello.c
#include <stdio.h>
int main()
{
//这是第一个C语言代码
printf("hello world\n");
return 0;
}
C语言的源代码文件是一个普通的文本文件,但扩展名必须是.c。
代码分析
1) include头文件包含
- #include的意思是头文件包含,#include <stdio.h>代表包含stdio.h这个头文件
- 使用C语言库函数需要提前包含库函数对应的头文件,如这里使用了printf()函数,需要包含stdio.h头文件
#include< > 与 #include " "的区别:
- < > 表示系统直接按系统指定的目录检索
- " " 表示系统先在 " " 指定的路径(没写路径代表当前路径)查找头文件,如果找不到,再按系统指定的目录检索
2) main函数
- 一个完整的C语言程序,是由一个、且只能有一个main()函数(又称主函数,必须有)和若干个其他函数结合而成(可选)。
- main函数是C语言程序的入口,程序是从main函数开始执行。
3) { } 括号,程序体和代码块
- { }叫代码块,一个代码块内部可以有一条或者多条语句
- C语言每句可执行代码都是";"分号结尾
- 所有的#开头的行,都代表预编译指令,预编译指令行结尾是没有分号的
- 所有的可执行语句必须是在代码块里面
4) 注释
- //叫行注释,注释的内容编译器是忽略的,注释主要的作用是在代码中加一些说明和解释,这样有利于代码的阅读
- /**/叫块注释
- 块注释是C语言标准的注释方法
- 行注释是从C++语言借鉴过来的
5) printf函数
- printf是C语言库函数,功能是向标准输出设备输出一个字符串
- printf(“hello world\n”);//\n的意思是回车换行
6) return语句
- return代表函数执行完毕,返回return代表函数的终止
- 如果main定义的时候前面是int,那么return后面就需要写一个整数;如果main定义的时候前面是void,那么return后面什么也不需要写
- 在main函数中return 0代表程序执行成功,return -1代表程序执行失败
- int main()和void main()在C语言中是一样的,但C++只接受int main这种定义方式
2. 数据类型
1. 关键字
2. 数据类型
3. 常量
- 在程序运行过程中,其值不能被改变的量
- 常量一般出现在表达式或赋值语句中
4. 变量
- 变量:
- 在程序运行过程中,其值可以改变
- 变量在使用前必须先定义,定义变量前必须有相应的数据类型
标识符命名规则:
- 标识符不能是关键字
- 标识符只能由字母、数字、下划线组成
- 第一个字符必须为字母或下划线
- 标识符中字母区分大小写
变量特点:
- 变量在编译时为其分配相应的内存空间
- 可以通过其名字和地址访问相应内存
- 声明和定义区别
- 声明变量不需要建立存储空间,如:extern int a;
- 定义变量需要建立存储空间,如:int b;
从广义的角度来讲声明中包含着定义,即定义是声明的一个特例,所以并非所有的声明都是定义:
- int b 它既是声明,同时又是定义
- 对于 extern b来讲它只是声明不是定义
一般的情况下,把建立存储空间的声明称之为“定义”,而把不需要建立存储空间的声明称之为“声明”。
- 类型限定符
限定符 | 含义 |
---|---|
extern | 声明一个变量,extern声明的变量没有建立存储空间。extern int a; |
const | 定义一个常量,常量的值不能修改。const int a = 10; |
volatile | 防止编译器优化代码 |
register | 定义寄存器变量,提高效率。register是建议型的指令,而不是命令型的指令,如果CPU有空闲寄存器,那么register就生效,如果没有空闲寄存器,那么register无效。 |
2.1 基本类型
1. 整型:int
- 整型变量的定义和输出
打印格式 | 含义 |
---|---|
%d | 输出一个有符号的 10 进制 int 类型 |
%o(字母o) | 输出 8 进制的 int 类型 |
%X | 输出 16 进制的 int 类型 |
%u | 输出一个 10 进制的无符号数 |
- short、int、long、long long
数据类型 | 占用空间 |
---|---|
short(短整型) | 2 字节 |
int(整型) | 4 字节 |
long(长整形) | Windows为 4 字节,Linux为 4 字节(32位),8 字节(64位) |
long long(长长整形) | 8 字节 |
注意:
- 需要注意的是,整型数据在内存中占的字节数与所选择的操作系统有关。虽然 C 语言标准中没有明确规定整型数据的长度,但 long 类型整数的长度不能短于 int 类型, short 类型整数的长度不能短于 int 类型。
- 当一个小的数据类型赋值给一个大的数据类型,不会出错,因为编译器会自动转化。但当一个大的类型赋值给一个小的数据类型,那么就可能丢失高位。
打印格式 | 含义 |
---|---|
%hd | 输出 short 类型 |
%d | 输出 int 类型 |
%l | 输出 long 类型 |
%ll | 输出 long long 类型 |
%hu | 输出 unsigned short 类型 |
%u | 输出 unsigned int 类型 |
%lu | 输出 unsigned long 类型 |
%llu | 输出 unsigned long long 类型 |
当我们写程序要处理一个不可能出现负值的时候,一般用无符号数,这样可以增大数的表达最大值。
- 有符号数和无符号数区别
- 有符号数是最高位为符号位,0代表正数,1代表负数。
- 无符号数最高位不是符号位,而就是数的一部分,无符号数不可能是负数。
有符号和无符号整型取值范围
数据类型 | 占用空间 | 取值范围 |
---|---|---|
short | 2字节 | -32768 到 32767 (-215 ~ 215-1) |
int | 4字节 | -2147483648 到 2147483647 (-231 ~ 231-1) |
long | 4字节 | -2147483648 到 2147483647 (-231 ~ 231-1) |
unsigned short | 2字节 | 0 到 65535 (0 ~ 216-1) |
unsigned int | 4字节 | 0 到 4294967295 (0 ~ 232-1) |
unsigned long | 4字节 | 0 到 4294967295 (0 ~ 232-1) |
2. 字符型:char
- 字符变量的定义和输出
- 字符型变量用于存储一个单一字符,在 C 语言中用 char 表示,其中每个字符变量都会占用 1 个字节。在给字符型变量赋值时,需要用一对英文半角格式的单引号(’ ')把字符括起来。
- 字符变量实际上并不是把该字符本身放到变量的内存单元中去,而是将该字符对应的 ASCII 编码放到变量的存储单元中。char的本质就是一个1字节大小的整型。
- ASCII对照表
ASCII值 | 控制字符 | ASCII值 | 字符 | ASCII值 | 字符 | ASCII值 | 字符 |
---|---|---|---|---|---|---|---|
0 | NUT | 32 | (space) | 64 | @ | 96 | 、 |
1 | SOH | 33 | ! | 65 | A | 97 | a |
2 | STX | 34 | " | 66 | B | 98 | b |
3 | ETX | 35 | # | 67 | C | 99 | c |
4 | EOT | 36 | $ | 68 | D | 100 | d |
5 | ENQ | 37 | % | 69 | E | 101 | e |
6 | ACK | 38 | & | 70 | F | 102 | f |
7 | BEL | 39 | , | 71 | G | 103 | g |
8 | BS | 40 | ( | 72 | H | 104 | h |
9 | HT | 41 | ) | 73 | I | 105 | i |
10 | LF | 42 | * | 74 | J | 106 | j |
11 | VT | 43 | + | 75 | K | 107 | k |
12 | FF | 44 | , | 76 | L | 108 | l |
13 | CR | 45 | - | 77 | M | 109 | m |
14 | SO | 46 | . | 78 | N | 110 | n |
15 | SI | 47 | / | 79 | O | 111 | o |
16 | DLE | 48 | 0 | 80 | P | 112 | p |
17 | DCI | 49 | 1 | 81 | Q | 113 | q |
18 | DC2 | 50 | 2 | 82 | R | 114 | r |
19 | DC3 | 51 | 3 | 83 | S | 115 | s |
20 | DC4 | 52 | 4 | 84 | T | 116 | t |
21 | NAK | 53 | 5 | 85 | U | 117 | u |
22 | SYN | 54 | 6 | 86 | V | 118 | v |
23 | TB | 55 | 7 | 87 | W | 119 | w |
24 | CAN | 56 | 8 | 88 | X | 120 | x |
25 | EM | 57 | 9 | 89 | Y | 121 | y |
26 | SUB | 58 | : | 90 | Z | 122 | z |
27 | ESC | 59 | ; | 91 | [ | 123 | { |
28 | FS | 60 | < | 92 | / | 124 | | |
29 | GS | 61 | = | 93 | ] | 125 | } |
30 | RS | 62 | > | 94 | ^ | 126 | ` |
31 | US | 63 | ? | 95 | _ | 127 | DEL |
ASCII 码大致由以下两部分组成:
- ASCII 非打印控制字符: ASCII 表上的数字 0-31 分配给了控制字符,用于控制像打印机等一些外围设备。
- ASCII 打印字符:数字 32-126 分配给了能在键盘上找到的字符,当查看或打印文档时就会出现。数字 127 代表 Del 命令。
- 转义字符
转义字符 | 含义 | ASCII码值(十进制) |
---|---|---|
\a | 警报 | 007 |
\b | 退格(BS) ,将当前位置移到前一列 | 008 |
\f | 换页(FF),将当前位置移到下页开头 | 012 |
\n | 换行(LF) ,将当前位置移到下一行开头 | 010 |
\r | 回车(CR) ,将当前位置移到本行开头 | 013 |
\t | 水平制表(HT) (跳到下一个 TAB 位置) | 009 |
\v | 垂直制表(VT) | 011 |
\ | 代表一个反斜线字符"" | 092 |
’ | 代表一个单引号(撇号)字符 | 039 |
" | 代表一个双引号字符 | 034 |
? | 代表一个问号 | 063 |
\0 | 数字 0 | 000 |
\ddd | 8 进制转义字符,d 范围0 ~ 7 | 3 位 8 进制 |
\xhh | 16 进制转义字符,h范围0 ~ 7,a ~ f,A ~ F | 3 位 16 进制 |
注意:红色字体标注的为不可打印字符。
- 数值溢出
- 当超过一个数据类型能够存放最大的范围时,数值会溢出。
- 有符号位最高位溢出的区别:符号位溢出会导致数的正负发生改变,但最高位的溢出会导致最高位丢失。
数据类型 | 占用空间 | 取值范围 |
---|---|---|
char | 1 字节 | -128到 127(-27 ~ 27-1) |
unsigned char | 1 字节 | 0 到 255(0 ~ 28-1) |
3. 实型(浮点型):float、double
实型变量也可以称为浮点型变量,浮点型变量是用来存储小数数值的。在C语言中, 浮点型变量分为两种: 单精度浮点数(float)、 双精度浮点数(double), 但是double型变量所表示的浮点数比 float 型变量更精确。
数据类型 | 占用空间 | 取值范围 |
---|---|---|
float | 4 字节 | 7 位有效数字 |
double | 8 字节 | 15 ~ 16 位有效数字 |
由于浮点型变量是由有限的存储单元组成的,因此只能提供有限的有效数字。在有效位以外的数字将被舍去,这样可能会产生一些误差。
不以 f 结尾的常量是 double 类型,以 f 结尾的常量(如 3.14f)是 float 类型。
#include <stdio.h>
int main()
{
//传统方式赋值
float a = 3.14f; //或3.14F
double b = 3.14;
printf("a = %f\n", a);
printf("b = %lf\n", b);
//科学法赋值
a = 3.2e3f; //3.2*1000 = 32000,e可以写E
printf("a1 = %f\n", a);
a = 100e-3f; //100*0.001 = 0.1
printf("a2 = %f\n", a);
a = 3.1415926f;
printf("a3 = %f\n", a); //结果为3.141593
return 0;
}
2.2 构造类型
1. 数组和字符串
概述
在程序设计中,为了方便处理数据把具有相同类型的若干变量按有序形式组织起来——称为数组。
数组就是在内存中连续的相同类型的变量空间。同一个数组所有的成员都是相同的数据类型,同时所有的成员在内存中的地址是连续的。
数组属于构造数据类型:
- 一个数组可以分解为多个数组元素:这些数组元素可以是基本数据类型或构造类型。
- 按数组元素类型的不同,数组可分为:数值数组、字符数组、指针数组、结构数组等类别。
通常情况下,数组元素下标的个数也称为维数,根据维数的不同,可将数组分为一维数组、二维数组、三维数组、四维数组等。通常情况下,我们将二维及以上的数组称为多维数组。
1. 一维数组
- 一维数组的定义和使用
- 数组名字符合标识符的书写规定(数字、英文字母、下划线)
- 数组名不能与其它变量名相同,同一作用域内是唯一的
- 方括号[ ]中常量表达式表示数组元素的个数
int a[3]表示数组 a 有 3 个元素
其下标从 0 开始计算,因此 3 个元素分别为 a[0], a[1], a[2]
- 定义数组时[]内最好是常量,使用数组时[ ]内即可是常量,也可以是变量
- 一维数组的初始化
在定义数组的同时进行赋值,称为初始化。全局数组若不初始化,编译器将其初始化为零。局部数组若不初始化,内容为随机值。
int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };//定义一个数组,同时初始化所有成员变量
int a[10] = { 1, 2, 3 };//初始化前三个成员,后面所有元素都设置为0
int a[10] = { 0 };//所有的成员都设置为0
//[]中不定义元素个数,定义时必须初始化
int a[] = { 1, 2, 3, 4, 5 };//定义了一个数组,有5个成员
- 数组名
数组名是一个地址的常量,代表数组中首元素的地址。
2. 二维数组
- 二维数组的定义和使用
二维数组定义的一般形式是:
类型说明符 数组名[常量表达式1][常量表达式2]
其中常量表达式 1 表示第一维下标的长度,常量表达式 2 表示第二维下标的长度。
int a[3][4];
- 命名规则同一维数组
- 定义了一个三行四列的数组,数组名为 a 其元素类型为整型,该数组的元素个数为 3×4 个,即:
二维数组 a 是按行进行存放的,先存放 a[0] 行,再存放 a[1] 行、a[2] 行,并且每行有四个元素,也是依次存放的。
- 二维数组在概念上是二维的:其下标在两个方向上变化,对其访问一般需要两个下标。
- 在内存中并不存在二维数组,二维数组实际的硬件存储器是连续编址的,也就是说内存中只有一维数组,即放完一行之后顺次放入第二行,和一维数组存放方式是一样的。
- 二维数组的初始化
//分段赋值 int a[3][4] = {{ 1, 2, 3, 4 },{ 5, 6, 7, 8, },{ 9, 10, 11, 12 }};
int a[3][4] =
{
{ 1, 2, 3, 4 },
{ 5, 6, 7, 8, },
{ 9, 10, 11, 12 }
};
//连续赋值
int a[3][4] = { 1, 2, 3, 4 , 5, 6, 7, 8, 9, 10, 11, 12 };
//可以只给部分元素赋初值,未初始化则为0
int a[3][4] = { 1, 2, 3, 4 };
//所有的成员都设置为0
int a[3][4] = {0};
//[]中不定义元素个数,定义时必须初始化
int a[][4] = { 1, 2, 3, 4, 5, 6, 7, 8};
- 数组名
数组名是一个地址的常量,代表数组中首元素的地址。
3. 多维数组
多维数组的定义与二维数组类似,其语法格式具体如下:
数组类型修饰符 数组名 [n1][n2]…[nn];
int a[3][4][5];
定义了一个三维数组,数组的名字是 a,数组的长度为 3,每个数组的元素又是一个二维数组,这个二维数组的长度是 4,并且这个二维数组中的每个元素又是一个一维数组,这个一维数组的长度是 5,元素类型是 int。
4. 字符数组与字符串
- 字符数组与字符串区别
- C语言中没有字符串这种数据类型,可以通过char的数组来替代;
- 字符串一定是一个 char 的数组,但 char 的数组未必是字符串;
- 数字 0(和字符 ‘\0’ 等价)结尾的 char 数组就是一个字符串,但如果 char 数组没有以数字 0 结尾,那么就不是一个字符串,只是普通字符数组,所以字符串是一种特殊的 char 的数组。
#include <stdio.h>
int main()
{
char c1[] = { 'c', ' ', 'p', 'r', 'o', 'g' }; //普通字符数组
printf("c1 = %s\n", c1); //乱码,因为没有’\0’结束符
//以‘\0’(‘\0’就是数字0)结尾的字符数组是字符串
char c2[] = { 'c', ' ', 'p', 'r', 'o', 'g', '\0'};
printf("c2 = %s\n", c2);
//字符串处理以‘\0’(数字0)作为结束符,后面的'h', 'l', 'l', 'e', 'o'不会输出
char c3[] = { 'c', ' ', 'p', 'r', 'o', 'g', '\0', 'h', 'l', 'l', 'e', 'o', '\0'};
printf("c3 = %s\n", c3);
return 0;
}
- 字符串的初始化
#include <stdio.h>
// C语言没有字符串类型,通过字符数组模拟
// C语言字符串,以字符‘\0’, 数字0
int main()
{
//不指定长度, 没有0结束符,有多少个元素就有多长
char buf[] = { 'a', 'b', 'c' };
printf("buf = %s\n", buf); //乱码
//指定长度,后面没有赋值的元素,自动补0
char buf2[100] = { 'a', 'b', 'c' };
printf("buf2 = %s\n", buf2);
//所有元素赋值为0
char buf3[100] = { 0 };
//char buf4[2] = { '1', '2', '3' };//数组越界
char buf5[50] = { '1', 'a', 'b', '0', '7' };
printf("buf5 = %s\n", buf5);
char buf6[50] = { '1', 'a', 'b', 0, '7' };
printf("buf6 = %s\n", buf6);
char buf7[50] = { '1', 'a', 'b', '\0', '7' };
printf("buf7 = %s\n", buf7);
//使用字符串初始化,编译器自动在后面补0,常用
char buf8[] = "agjdslgjlsdjg";
//'\0'后面最好不要连着数字,有可能几个数字连起来刚好是一个转义字符
//'\ddd'八进制字义字符,'\xdd'十六进制转移字符
// \012相当于\n
char str[] = "\012abc";
printf("str == %s\n", str);
return 0;
}
- 字符串的输入输出
由于字符串采用了 ‘\0’ 标志,字符串的输入输出将变得简单方便。
#include <stdio.h>
int main()
{
char str[100];
printf("input string1 : \n");
scanf("%s", str);//scanf(“%s”,str)默认以空格分隔
printf("output:%s\n", str);
return 0;
}
2. 结构体:struct
1. 概述
数组:描述一组具有相同类型数据的有序集合,用于处理大量相同类型的数据运算。
有时我们需要将不同类型的数据组合成一个有机的整体,如:一个学生有学号/姓名/性别/年龄/地址等属性。显然单独定义以上变量比较繁琐,数据不便于管理。
C语言中给出了另一种构造数据类型——结构体。
2. 结构体变量的定义和初始化
定义结构体变量的方式:
- 先声明结构体类型再定义变量名
- 在声明类型的同时定义变量
- 直接定义结构体类型变量(无类型名)
结构体类型和结构体变量关系:
- 结构体类型:指定了一个结构体类型,它相当于一个模型,但其中并无具体数据,系统对之也不分配实际内存单元。
- 结构体变量:系统根据结构体类型(内部成员状况)为之分配空间。
//结构体类型的定义
struct stu
{
char name[50];
int age;
};
//先定义类型,再定义变量(常用)
struct stu s1 = { "mike", 18 };
//定义类型同时定义变量
struct stu2
{
char name[50];
int age;
}s2 = { "lily", 22 };
struct
{
char name[50];
int age;
}s3 = { "yuri", 25 };
3. 结构体成员的使用
#include<stdio.h>
#include<string.h>
//结构体类型的定义
struct stu
{
char name[50];
int age;
};
int main()
{
struct stu s1;
//如果是普通变量,通过点运算符操作结构体成员
strcpy(s1.name, "abc");
s1.age = 18;
printf("s1.name = %s, s1.age = %d\n", s1.name, s1.age);
//如果是指针变量,通过->操作结构体成员
strcpy((&s1)->name, "test");
(&s1)->age = 22;
printf("(&s1)->name = %s, (&s1)->age = %d\n", (&s1)->name, (&s1)->age);
return 0;
}
4. 结构体数组
#include <stdio.h>
//统计学生成绩
struct stu
{
int num;
char name[20];
char sex;
float score;
};
int main()
{
//定义一个含有5个元素的结构体数组并将其初始化
struct stu boy[5] = {
{ 101, "Li ping", 'M', 45 },
{ 102, "Zhang ping", 'M', 62.5 },
{ 103, "He fang", 'F', 92.5 },
{ 104, "Cheng ling", 'F', 87 },
{ 105, "Wang ming", 'M', 58 }};
int i = 0;
int c = 0;
float ave, s = 0;
for (i = 0; i < 5; i++)
{
s += boy[i].score; //计算总分
if (boy[i].score < 60)
{
c += 1; //统计不及格人的分数
}
}
printf("s=%f\n", s);//打印总分数
ave = s / 5; //计算平均分数
printf("average=%f\ncount=%d\n\n", ave, c); //打印平均分与不及格人数
for (i = 0; i < 5; i++)
{
printf(" name=%s, score=%f\n", boy[i].name, boy[i].score);
// printf(" name=%s, score=%f\n", (boy+i)->name, (boy+i)->score);
}
return 0;
}
5. 结构体套结构体
#include <stdio.h>
struct person
{
char name[20];
char sex;
};
struct stu
{
int id;
struct person info;
};
int main()
{
struct stu s[2] = { 1, "lily", 'F', 2, "yuri", 'M' };
int i = 0;
for (i = 0; i < 2; i++)
{
printf("id = %d\tinfo.name=%s\tinfo.sex=%c\n", s[i].id, s[i].info.name, s[i].info.sex);
}
return 0;
}
6. 结构体赋值
#include<stdio.h>
#include<string.h>
//结构体类型的定义
struct stu
{
char name[50];
int age;
};
int main()
{
struct stu s1;
//如果是普通变量,通过点运算符操作结构体成员
strcpy(s1.name, "abc");
s1.age = 18;
printf("s1.name = %s, s1.age = %d\n", s1.name, s1.age);
//相同类型的两个结构体变量,可以相互赋值
//把s1成员变量的值拷贝给s2成员变量的内存
//s1和s2只是成员变量的值一样而已,它们还是没有关系的两个变量
struct stu s2 = s1;
//memcpy(&s2, &s1, sizeof(s1));
printf("s2.name = %s, s2.age = %d\n", s2.name, s2.age);
return 0;
}
7. 结构体和指针
1)指向普通结构体变量的指针
#include<stdio.h>
//结构体类型的定义
struct stu
{
char name[50];
int age;
};
int main()
{
struct stu s1 = { "lily", 18 };
//如果是指针变量,通过->操作结构体成员
struct stu * p = &s1;
printf("p->name = %s, p->age=%d\n", p->name, p->age);
printf("(*p).name = %s, (*p).age=%d\n", (*p).name, (*p).age);
return 0;
}
2)堆区结构体变量
#include<stdio.h>
#include <string.h>
#include <stdlib.h>
//结构体类型的定义
struct stu
{
char name[50];
int age;
};
int main()
{
struct stu *p = NULL;
p = (struct stu *)malloc(sizeof(struct stu));
//如果是指针变量,通过->操作结构体成员
strcpy(p->name, "test");
p->age = 22;
printf("p->name = %s, p->age=%d\n", p->name, p->age);
printf("(*p).name = %s, (*p).age=%d\n", (*p).name, (*p).age);
free(p);
p = NULL;
return 0;
}
3)结构体套一级指针
#include<stdio.h>
#include <string.h>
#include <stdlib.h>
//结构体类型的定义
struct stu
{
char * name; //一级指针
int age;
};
int main()
{
struct stu * p = NULL;
p = (struct stu *)malloc(sizeof(struct stu));
p->name = malloc(strlen("test") + 1);
strcpy(p->name, "test");
p->age = 22;
printf("p->name = %s, p->age=%d\n", p->name, p->age);
printf("(*p).name = %s, (*p).age=%d\n", (*p).name, (*p).age);
if (p->name != NULL)
{
free(p->name);
p->name = NULL;
}
if (p != NULL)
{
free(p);
p = NULL;
}
return 0;
}
8. 结构体做函数参数
1)结构体普通变量做函数参数
#include<stdio.h>
#include <string.h>
//结构体类型的定义
struct stu
{
char name[50];
int age;
};
//函数参数为结构体普通变量
void set_stu(struct stu tmp)
{
strcpy(tmp.name, "mike");
tmp.age = 18;
printf("tmp.name = %s, tmp.age = %d\n", tmp.name, tmp.age);
}
int main()
{
struct stu s = { 0 };
set_stu(s); //值传递
printf("s.name = %s, s.age = %d\n", s.name, s.age);
return 0;
}
2)结构体指针变量做函数参数
#include<stdio.h>
#include <string.h>
//结构体类型的定义
struct stu
{
char name[50];
int age;
};
//函数参数为结构体指针变量
void set_stu_pro(struct stu *tmp)
{
strcpy(tmp->name, "mike");
tmp->age = 18;
}
int main()
{
struct stu s = { 0 };
set_stu_pro(&s); //地址传递
printf("s.name = %s, s.age = %d\n", s.name, s.age);
return 0;
}
3)结构体数组名做函数参数
#include<stdio.h>
//结构体类型的定义
struct stu
{
char name[50];
int age;
};
//void set_stu_pro(struct stu tmp[100], int n)
//void set_stu_pro(struct stu tmp[], int n)
void set_stu_pro(struct stu *tmp, int n)
{
int i = 0;
for (i = 0; i < n; i++)
{
sprintf(tmp->name, "name%d%d%d", i, i, i);
tmp->age = 20 + i;
tmp++;
}
}
int main()
{
struct stu s[3] = { 0 };
int i = 0;
int n = sizeof(s) / sizeof(s[0]);
set_stu_pro(s, n); //数组名传递
for (i = 0; i < n; i++)
{
printf("%s, %d\n", s[i].name, s[i].age);
}
return 0;
}
4)const 修饰结构体指针形参变量
//结构体类型的定义
struct stu
{
char name[50];
int age;
};
void fun1(struct stu * const p)
{
//p = NULL; //err
p->age = 10; //ok
}
//void fun2(struct stu const* p)
void fun2(const struct stu * p)
{
p = NULL; //ok
//p->age = 10; //err
}
void fun3(const struct stu * const p)
{
//p = NULL; //err
//p->age = 10; //err
}
3. 联合体:union
- 联合union是一个能在同一个存储空间存储不同类型数据的类型;
- 联合体所占的内存长度等于其最长成员的长度,也有叫做共用体;
- 同一内存段可以用来存放几种不同类型的成员,但每一瞬时只有一种起作用;
- 共用体变量中起作用的成员是最后一次存放的成员,在存入一个新的成员后原有的成员的值会被覆盖;
- 共用体变量的地址和它的各成员的地址都是同一地址。
#include <stdio.h>
//共用体也叫联合体
union Test
{
unsigned char a;
unsigned int b;
unsigned short c;
};
int main()
{
//定义共用体变量
union Test tmp;
//1、所有成员的首地址是一样的
printf("%p, %p, %p\n", &(tmp.a), &(tmp.b), &(tmp.c));
//2、共用体大小为最大成员类型的大小
printf("%lu\n", sizeof(union Test));
//3、一个成员赋值,会影响另外的成员
//左边是高位,右边是低位
//低位放低地址,高位放高地址
tmp.b = 0x44332211;
printf("%x\n", tmp.a); //11
printf("%x\n", tmp.c); //2211
tmp.a = 0x00;
printf("short: %x\n", tmp.c); //2200
printf("int: %x\n", tmp.b); //44332200
return 0;
}
4. 枚举:enum
枚举:将变量的值一一列举出来,变量的值只限于列举出来的值的范围内。
枚举类型定义:
enum 枚举名
{
枚举值表
};
- 在枚举值表中应列出所有可用值,也称为枚举元素。
- 枚举值是常量,不能在程序中用赋值语句再对它赋值。
- 举元素本身由系统定义了一个表示序号的数值从0开始顺序定义为0,1,2 …
#include <stdio.h>
enum weekday
{
sun = 2, mon, tue, wed, thu, fri, sat
} ;
enum bool
{
flase, true
};
int main()
{
enum weekday a, b, c;
a = sun;
b = mon;
c = tue;
printf("%d,%d,%d\n", a, b, c);
enum bool flag;
flag = true;
if (flag == 1)
{
printf("flag为真\n");
}
return 0;
}
5. typedef
typedef为C语言的关键字,作用是为一种数据类型(基本类型或自定义数据类型)定义一个新名字,不能创建新类型。
- 与#define不同,typedef仅限于数据类型,而不是能是表达式或具体的值
- #define发生在预处理,typedef发生在编译阶段
#include <stdio.h>
typedef int INT;
typedef char BYTE;
typedef BYTE T_BYTE;
typedef unsigned char UBYTE;
typedef struct type
{
UBYTE a;
INT b;
T_BYTE c;
}TYPE, *PTYPE;
int main()
{
TYPE t;
t.a = 254;
t.b = 10;
t.c = 'c';
PTYPE p = &t;
printf("%u, %d, %c\n", p->a, p->b, p->c);
return 0;
}
2.3 指针类型
- 内存区的每一个字节都有一个编号,这就是“地址”。
- 如果在程序中定义了一个变量,在对程序进行编译或运行时,系统就会给这个变量分配内存单元,并确定它的内存地址(编号)
- 指针的实质就是内存“地址”。指针就是地址,地址就是指针。
- 指针是内存单元的编号,指针变量是存放地址的变量。
- 通常我们叙述时会把指针变量简称为指针,实际他们含义并不一样
1. 指针基础知识
1. 指针变量的定义和使用
- 指针也是一种数据类型,指针变量也是一种变量
- 指针变量指向谁,就把谁的地址赋值给指针变量
- “*” 操作符操作的是指针变量指向的内存空间
#include <stdio.h>
int main()
{
int a = 0;
char b = 100;
printf("%p, %p\n", &a, &b); //打印a, b的地址
//int *代表是一种数据类型,int*指针类型,p才是变量名
//定义了一个指针类型的变量,可以指向一个int类型变量的地址
int *p;
p = &a;//将a的地址赋值给变量p,p也是一个变量,值是一个内存地址编号
printf("%d\n", *p);//p指向了a的地址,*p就是a的值
char *p1 = &b;
printf("%c\n", *p1);//*p1指向了b的地址,*p1就是b的值
return 0;
}
注意:&可以取得一个变量在内存中的地址。但是,不能取寄存器变量,因为寄存器变量不在内存里,而在CPU里面,所以是没有地址的。
2. 通过指针间接修改变量的值
int a = 0;
int b = 11;
int *p = &a;
*p = 100;
printf("a = %d, *p = %d\n", a, *p);
p = &b;
*p = 22;
printf("b = %d, *p = %d\n", b, *p);
3. 指针大小
- 使用sizeof()测量指针的大小,得到的总是:4或8
- sizeof()测的是指针变量指向存储地址的大小
- 在32位平台,所有的指针(地址)都是32位(4字节)
- 在64位平台,所有的指针(地址)都是64位(8字节)
4. 野指针、空指针、万能指针void *
指针变量也是变量,是变量就可以任意赋值,不要越界即可(32位为4字节,64位为8字节),但是,任意数值赋值给指针变量没有意义,因为这样的指针就成了野指针,此指针指向的区域是未知(操作系统不允许操作此指针指向的内存区域)。所以,野指针不会直接引发错误,操作野指针指向的内存区域才会出问题。
int a = 100;
int *p;
p = a; //把a的值赋值给指针变量p,p为野指针, ok,不会有问题,但没有意义
p = 0x12345678; //给指针变量p赋值,p为野指针, ok,不会有问题,但没有意义
*p = 1000; //操作野指针指向未知区域,内存出问题,err
但是,野指针和有效指针变量保存的都是数值,为了标志此指针变量没有指向任何变量(空闲可用),C语言中,可以把NULL赋值给此指针,这样就标志此指针为空指针,没有任何指针。
int *p = NULL;
NULL是一个值为0的宏常量:
#define NULL ((void *)0)
void * 指针可以指向任意变量的内存空间:
void *p = NULL;
int a = 10;
p = (void *)&a; //指向变量时,最好转换为void *
//使用指针变量指向的内存时,转换为int *
*( (int *)p ) = 11;
printf("a = %d\n", a);
5. const修饰的指针变量
int a = 100;
int b = 200;
//指向常量的指针
//修饰*,指针指向内存区域不能修改,指针指向可以变
const int * p1 = &a; //等价于int const * p1 = &a; 不能更改值大小,可以更改地址
//*p1 = 111; //err
p1 = &b; //ok
//指针常量
//修饰p1,指针指向不能变,指针指向的内存可以修改
int * const p2 = &a; //能更改值大小,不可以更改地址
//p2 = &b; //err
*p2 = 222; //ok
2. 指针和数组
1. 数组名
数组名字是数组的首元素地址,但它是一个常量:
int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
printf("a = %p\n", a);
printf("&a[0] = %p\n", &a[0]);
//a = 10; //err, 数组名只是常量,不能修改
2. 指针操作数组元素
#include <stdio.h>
int main()
{
int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int i = 0;
int n = sizeof(a) / sizeof(a[0]);
for (i = 0; i < n; i++)
{
//printf("%d, ", a[i]);
printf("%d, ", *(a+i));
}
printf("\n");
int * p = a; //定义一个指针变量保存a的地址
for (i = 0; i < n; i++)
{
p[i] = 2 * i; //p[i] = a[i]
}
for (i = 0; i < n; i++)
{
printf("%d, ", *(p + i));
}
printf("\n");
return 0;
}
输出结果:
3. 指针加减运算
1)加法运算
- 指针计算不是简单的整数相加
- 如果是一个int *,+1的结果是增加一个int的大小
- 如果是一个char *,+1的结果是增加一个char大小
#include <stdio.h>
int main()
{
int a;
int *p = &a;
printf("%d\n", p);
p += 2;//移动了2个int
printf("%d\n", p);
char b = 0;
char *p1 = &b;
printf("%d\n", p1);
p1 += 2;//移动了2个char
printf("%d\n", p1);
return 0;
}
通过改变指针指向操作数组元素:
#include <stdio.h>
int main()
{
int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int i = 0;
int n = sizeof(a) / sizeof(a[0]);
int *p = a;
for (i = 0; i < n; i++)
{
printf("%d, ", *p);
p++; //p = p + 1
}
printf("\n");
return 0;
}
2)减法运算
#include <stdio.h>
int main()
{
int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int i = 0;
int n = sizeof(a) / sizeof(a[0]);
int *p = a+n-1;
for (i = 0; i < n; i++)
{
printf("%d, ", *p);
p--;
}
printf("\n");
return 0;
}
4. 指针数组
指针数组,它是数组,数组的每个元素都是指针类型。
#include <stdio.h>
int main()
{
//指针数组
int *p[3];
int a = 1;
int b = 2;
int c = 3;
int i = 0;
p[0] = &a;
p[1] = &b;
p[2] = &c;
for (i = 0; i < sizeof(p) / sizeof(p[0]); i++ )
{
printf("%d, ", *(p[i]));
}
printf("\n");
return 0;
}
3. 多级指针
- C语言允许有多级指针存在,在实际的程序中一级指针最常用,其次是二级指针。
- 二级指针就是指向一个一级指针变量地址的指针。
- 三级指针基本用不着。
int a = 10;
int *p = &a; //一级指针
*p = 100; //*p就是a
int **q = &p;
//*q就是p
//**q就是a
int ***t = &q;
//*t就是q
//**t就是p
//***t就是a
4. 指针和函数
1. 函数形参改变实参的值
#include <stdio.h>
void swap1(int x, int y)
{
int tmp;
tmp = x;
x = y;
y = tmp;
printf("x = %d, y = %d\n", x, y);
}
void swap2(int *x, int *y)
{
int tmp;
tmp = *x;
*x = *y;
*y = tmp;
}
int main()
{
int a = 3;
int b = 5;
swap1(a, b); //值传递
printf("a = %d, b = %d\n", a, b);
a = 3;
b = 5;
swap2(&a, &b); //地址传递
printf("a2 = %d, b2 = %d\n", a, b);
return 0;
}
输出结果:
2. 数组名做函数参数
数组名做函数参数,函数的形参会退化为指针:
#include <stdio.h>
void printArrary(int * a, int n)
{
int i = 0;
for (i = 0; i < n; i++)
{
printf("%d, ", a[i]);
}
printf("\n");
}
int main()
{
int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int n = sizeof(a) / sizeof(a[0]);
//数组名做函数参数
printArrary(a, n);
return 0;
}
3. 指针做为函数的返回值
#include <stdio.h>
int a = 10;
int * getA()
{
return &a;
}
int main()
{
*( getA() ) = 111;
printf("a = %d\n", a);
return 0;
}
5. 指针和字符串
1. 字符指针
#include <stdio.h>
int main()
{
char str[] = "hello world"; //"hello world"字符串常量
char * p = str;
*p = 'm';
p++;
*p = 'i';
printf("%s\n", str);
p = "mike jiang";
printf("%s\n", p);
char * q = "test";
printf("%s\n", q);
return 0;
}
2. 字符指针做函数参数
#include <stdio.h>
void mystrcat(char *dest, const char *src)
{
int len1 = 0;
int len2 = 0;
while (dest[len1])
{
len1++;
}
while (src[len2])
{
len2++;
}
int i;
for (i = 0; i < len2; i++)
{
dest[len1 + i] = src[i];
}
}
int main()
{
char dst[100] = "hello mike";
char src[] = "123456";
mystrcat(dst, src);
printf("dst = %s\n", dst);
return 0;
}
6. 指针小结
定义 | 说明 |
---|---|
int i | 定义整形变量 |
int * p | 定义一个指向int的指针变量 |
int a[10] | 定义一个有10个元素的数组,每个元素类型为int |
int * p[10] | 定义一个有10个元素的数组,每个元素类型为int* |
int func() | 定义一个函数,返回值为int型 |
int * func() | 定义一个函数,返回值为int *型 |
int ** p | 定义一个指向int的指针的指针,二级指针 |
3. 程序设计
3.1 运算符与表达式
1. 常用运算符分类
运算符类型 | 作用 |
---|---|
算术运算符 | 用于处理四则运算 |
赋值运算符 | 用于将表达式的值赋给变量 |
比较运算符 | 用于表达式的比较,并返回一个真值或假值 |
逻辑运算符 | 用于根据表达式的值返回真值或假值 |
位运算符 | 用于处理数据的位运算 |
sizeof 运算符 | 用于求字节数长度 |
2. 算术运算符
运算符 | 术语 | 示例 | 结果 |
---|---|---|---|
+ | 正号 | +3 | 3 |
- | 负号 | -3 | -3 |
+ | 加 | 10 + 5 | 15 |
- | 减 | 10 - 5 | 5 |
* | 乘 | 10 * 5 | 50 |
/ | 除 | 10 / 5 | 2 |
% | 取模(取余) | 10 % 3 | 1 |
++ | 前自增 | a=2; b=++a; | a=3; b=3; |
++ | 后自增 | a=2; b=a++; | a=3; b=2; |
– | 前自减 | a=2; b=–a; | a=1; b=1; |
– | 后自减 | a=2; b=a–; | a=1; b=2; |
3. 赋值运算符
运算符 | 术语 | 示例 | 结果 |
---|---|---|---|
= | 赋值 | a=2; b=3; | a=2; b=3; |
+= | 加等于 | a=0; a+=2; | a=2; |
-= | 减等于 | a=5; a-=3; | a=2; |
*= | 乘等于 | a=2; a*=2; | a=4; |
/= | 除等于 | a=4; a/=2; | a=2; |
%= | 模等于 | a=3; a%2; | a=1; |
4. 比较运算符
C 语言的比较运算中, “真”用数字“1”来表示, “假”用数字“0”来表示。
运算符 | 术语 | 示例 | 结果 |
---|---|---|---|
== | 相等于 | 4 == 3 | 0 |
!= | 不等于 | 4 != 3 | 1 |
< | 小于 | 4 < 3 | 0 |
> | 大于 | 4 > 3 | 1 |
<= | 小于等于 | 4 <= 3 | 0 |
>= | 大于等于 | 4 >= 1 | 1 |
5. 逻辑运算符
运算符 | 术语 | 示例 | 结果 |
---|---|---|---|
! | 非 | !a | 如果 a 为假,则 !a 为真;如果 a 为真,则 !a 为假。 |
&& | 与 | a && b | 如果 a 和 b 都为真,则结果为真,否则为假。 |
|| | 或 | a || b | 如果a和b有一个为真,则结果为真,二者都为假时,结果为假。 |
6. 运算符优先级
7. 类型转换
数据有不同的类型,不同类型数据之间进行混合运算时必然涉及到类型的转换问题。
转换的方法有两种:
- 自动转换(隐式转换):遵循一定的规则,由编译系统自动完成。
- 强制类型转换:把表达式的运算结果强制转换成所需的数据类型。
类型转换的原则:占用内存字节数少(值域小)的类型,向占用内存字节数多(值域大)的类型转换,以保证精度不降低。
隐式转换
#include <stdio.h>
int main()
{
int num = 5;
printf("s1=%d\n", num / 2);
printf("s2=%lf\n", num / 2.0);
return 0;
}
强制转换
强制类型转换指的是使用强制类型转换运算符,将一个变量或表达式转化成所需的类型,其基本语法格式如下所示:
(类型说明符) (表达式)
#include <stdio.h>
int main()
{
float x = 0;
int i = 0;
x = 3.6f;
i = x; //x为实型, i为整型,直接赋值会有警告
i = (int)x; //使用强制类型转换
printf("x=%f, i=%d\n", x, i);
return 0;
}
3.2 程序流程结构
1. 概述
C语言支持最基本的三种程序运行结构:顺序结构、选择结构、循环结构。
- 顺序结构:程序按顺序执行,不发生跳转。
- 选择结构:依据是否满足条件,有选择的执行相应功能。
- 循环结构:依据条件是否满足,循环多次执行某段代码。
2. 选择结构
1. if 语句
#include <stdio.h>
int main()
{
int a = 1;
int b = 2;
if (a > b)
{
printf("%d\n", a);
}
return 0;
}
2. if…else语句
#include <stdio.h>
int main()
{
int a = 1;
int b = 2;
if (a > b)
{
printf("%d\n", a);
}
else
{
printf("%d\n", b);
}
return 0;
}
3. if…else if…else语句
#include <stdio.h>
int main()
{
unsigned int a;
scanf("%u", &a);
if (a < 10)
{
printf("个位\n");
}
else if (a < 100)
{
printf("十位\n");
}
else if (a < 1000)
{
printf("百位\n");
}
else
{
printf("很大\n");
}
return 0;
}
4. 三目运算符
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
int c;
if (a > b)
{
c = a;
}
else
{
c = b;
}
printf("c1 = %d\n", c);
a = 1;
b = 2;
c = ( a > b ? a : b );
printf("c2 = %d\n", c);
return 0;
}
5. switch语句
#include <stdio.h>
int main()
{
char c;
c = getchar();
switch (c) //参数只能是整型变量
{
case '1':
printf("OK\n");
break;//switch遇到break就中断了
case '2':
printf("not OK\n");
break;
default://如果上面的条件都不满足,那么执行default
printf("are u ok?\n");
}
return 0;
}
3. 循环结构
1. while语句
#include <stdio.h>
int main()
{
int a = 20;
while (a > 10)
{
scanf("%d", &a);
printf("a = %d\n", a);
}
return 0;
}
2. do…while语句
#include <stdio.h>
int main()
{
int a = 1;
do
{
a++;
printf("a = %d\n", a);
} while (a < 10);
return 0;
}
3. for语句
#include <stdio.h>
int main()
{
int i;
int sum = 0;
for (i = 0; i <= 100; i++)
{
sum += i;
}
printf("sum = %d\n", sum);
return 0;
}
4. 嵌套循环
循环语句之间可以相互嵌套:
#include <stdio.h>
int main()
{
int num = 0;
int i, j, k;
for (i = 0; i < 10; i++)
{
for (j = 0; j < 10; j++)
{
for (k = 0; k < 10; k++)
{
printf("hello world\n");
num++;
}
}
}
printf("num = %d\n", num);
return 0;
}
4. 跳转语句
1. break语句
在 switch 条件语句和循环语句中都可以使用 break 语句:
- 当它出现在switch条件语句中时,作用是终止某个case并跳出switch结构。
- 当它出现在循环语句中,作用是跳出当前内循环语句,执行后面的代码。
- 当它出现在嵌套循环语句中,跳出最近的内循环语句,执行后面的代码。
#include <stdio.h>
int main()
{
int i = 0;
while (1)
{
i++;
printf("i = %d\n", i);
if (i == 10)
{
break; //跳出while循环
}
}
int flag = 0;
int m = 0;
int n = 0;
for (m = 0; m < 10; m++)
{
for (n = 0; n < 10; n++)
{
if (n == 5)
{
flag = 1;
break; //跳出for (n = 0; n < 10; n++)
}
}
if (flag == 1)
{
break; //跳出for (m = 0; m < 10; m++)
}
}
return 0;
}
2. continue语句
在循环语句中,如果希望立即终止本次循环,并执行下一次循环,此时就需要使用 continue 语句。
#include<stdio.h>
int main()
{
int sum = 0; //定义变量sum
for (int i = 1; i <= 100; i++)
{
if (i % 2 == 0) //如果i是一个偶数,执行if语句中的代码
{
continue; //结束本次循环
}
sum += i; //实现sum和i的累加
}
printf("sum = %d\n", sum);
return 0;
}
3. goto语句(无条件跳转,尽量少用)
#include <stdio.h>
int main()
{
goto End; //无条件跳转到End的标识
printf("aaaaaaaaa\n");
End:
printf("bbbbbbbb\n");
return 0;
}
3.3 函数
1. 概述
- 函数分类
C 程序是由函数组成的,我们写的代码都是由主函数 main()开始执行的。函数是 C 程序的基本模块,是用于完成特定任务的程序代码单元。
从函数定义的角度看,函数可分为系统函数和用户定义函数两种:
- 系统函数,即库函数:这是由编译系统提供的,用户不必自己定义这些函数,可以直接使用它们,如我们常用的打印函数printf()。
- 用户定义函数:用以解决用户的专门需要。
- 函数的作用
- 函数的使用可以省去重复代码的编写,降低代码重复率
- 函数可以让程序更加模块化,从而有利于程序的阅读,修改和完善
假如我们编写一个实现以下功能的程序:读入一行数字;对数字进行排序;找到它们的平均值;打印出一个柱状图。如果我们把这些操作直接写在main()里,这样可能会给用户感觉代码会有点凌乱。但,假如我们使用函数,这样可以让程序更加清晰、模块化:
这里我们可以这么理解,程序就像公司,公司是由部门组成的,这个部门就类似于C程序的函数。默认情况下,公司就是一个大部门( 只有一个部门的情况下 ),相当于C程序的main()函数。如果公司比较小( 程序比较小 ),因为任务少而简单,一个部门即可( main()函数 )胜任。但是,如果这个公司很大( 大型应用程序 ),任务多而杂,如果只是一个部门管理( 相当于没有部门,没有分工 ),我们可想而知,公司管理、运营起来会有多混乱,不是说这样不可以运营,只是这样不完美而已,如果根据公司要求分成一个个部门( 根据功能封装一个一个函数 ),招聘由行政部门负责,研发由技术部门负责等,这样就可以分工明确,结构清晰,方便管理,各部门之间还可以相互协调。
2. 函数的定义
- 函数定义格式
函数定义的一般形式:
返回类型 函数名(形式参数列表)
{
数据定义部分;
执行语句部分;
}
- 函数名字、形参、函数体、返回值
函数名
理论上是可以随意起名字,最好起的名字见名知意,应该让用户看到这个函数名字就知道这个函数的功能。注意,函数名的后面有个圆换号(),代表这个为函数,不是普通的变量名。
形参列表
在定义函数时指定的形参,在未出现函数调用时,它们并不占内存中的存储单元,因此称它们是形式参数或虚拟参数,简称形参,表示它们并不是实际存在的数据,所以,形参里的变量不能赋值。
在定义函数时指定的形参,必须是,类型+变量的形式:
//1: right, 类型+变量
void max(int a, int b)
{
}
//2: error, 只有类型,没有变量
void max(int, int)
{
}
//3: error, 只有变量,没有类型
int a, int b;
void max(a, b)
{
}
在定义函数时指定的形参,可有可无,根据函数的需要来设计,如果没有形参,圆括号内容为空,或写一个void关键字:
// 没形参, 圆括号内容为空
void max()
{
}
// 没形参, 圆括号内容为void关键字
void max(void)
{
}
函数体
花括号{ }里的内容即为函数体的内容,这里为函数功能实现的过程,这和以前的写代码没太大区别,以前我们把代码写在main()函数里,现在只是把这些写到别的函数里。
返回值
函数的返回值是通过函数中的 return 语句获得的,return 后面的值也可以是一个表达式。
a) 尽量保证 return 语句中表达式的值和函数返回类型是同一类型。
int max() // 函数的返回值为int类型
{
int a = 10;
return a;// 返回值a为int类型,函数返回类型也是int,匹配
}
b) 如果函数返回的类型和 return 语句中表达式的值不一致,则以函数返回类型为准,即函数返回类型决定返回值的类型。对数值型数据,可以自动进行类型转换。
double max() // 函数的返回值为double类型
{
int a = 10;
return a;// 返回值a为int类型,它会转为double类型再返回
}
注意:如果函数返回的类型和return语句中表达式的值不一致,而它又无法自动进行类型转换,程序则会报错。
c) return 语句的另一个作用为中断 return 所在的执行函数,类似于 break 中断循环、switch 语句一样。
int max()
{
return 1;// 执行到,函数已经被中断,所以下面的return 2无法被执行到
return 2;// 没有执行
}
d) 如果函数带返回值,return 后面必须跟着一个值,如果函数没有返回值,函数名字的前面必须写一个 void 关键字,这时候,我们写代码时也可以通过 return 中断函数(也可以不用),只是这时,return 后面不带内容( 分号“;”除外)。
void max()// 最好要有void关键字
{
return; // 中断函数,这个可有可无
}
3. 函数的调用
定义函数后,我们需要调用此函数才能执行到这个函数里的代码段。这和main()函数不一样,main()为编译器设定好自动调用的主函数,无需人为调用,我们都是在main()函数里调用别的函数,一个 C 程序里有且只有一个main()函数。
- 函数执行流程
#include <stdio.h>
void print_test()
{
printf("this is for test\n");
}
int main()
{
print_test(); // print_test函数的调用
return 0;
}
1)进入main()函数
2)调用print_test()函数:
a.它会在main()函数的前寻找有没有一个名字叫“print_test”的函数定义;
b.如果找到,接着检查函数的参数,这里调用函数时没有传参,函数定义也没有形参,参数类型匹配;
c.开始执行print_test()函数,这时候,main()函数里面的执行会阻塞( 停 )在print_test()这一行代码,等待print_test()函数的执行。
3)print_test()函数执行完( 这里打印一句话 ),main()才会继续往下执行,执行到return 0, 程序执行完毕。
- 函数的形参和实参
- 形参出现在函数定义中,在整个函数体内都可以使用,离开该函数则不能使用。
- 实参出现在主调函数中,进入被调函数后,实参也不能使用。
- 实参变量对形参变量的数据传递是“值传递”,即单向传递,只由实参传给形参,而不能由形参传回来给实参。
- 在调用函数时,编译系统临时给形参分配存储单元。调用结束后,形参单元被释放。
- 实参单元与形参单元是不同的单元。调用结束后,形参单元被释放,函数调用结束返回主调函数后则不能再使用该形参变量。实参单元仍保留并维持原值。因此,在执行一个被调用函数时,形参的值如果发生改变,并不会改变主调函数中实参的值。
- 无参函数调用
// 函数的定义
void test()
{
}
int main()
{
// 函数的调用
test(); // right, 圆括号()不能省略
test(250); // error, 函数定义时没有参数
return 0;
}
如果是调用无参函数,则不能加上“实参”,但括号不能省略。
- 有参函数调用
a)如果实参表列包含多个实参,则各参数间用逗号隔开。
// 函数的定义
void test(int a, int b)
{
}
int main()
{
int p = 10, q = 20;
test(p, q); // 函数的调用
return 0;
}
b)实参与形参的个数应相等,类型应匹配(相同或赋值兼容)。实参与形参按顺序对应,一对一地传递数据。
c)实参可以是常量、变量或表达式,无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值,以便把这些值传送给形参。所以,这里的变量是在圆括号( )外面定义好、赋好值的变量。
// 函数的定义
void test(int a, int b)
{
}
int main()
{
// 函数的调用
int p = 10, q = 20;
test(p, q); // right
test(11, 30 - 10); // right
test(int a, int b); // error, 不应该在圆括号里定义变量
return 0;
}
- 函数返回值
a)如果函数定义没有返回值,函数调用时不能写void关键字,调用函数时也不能接收函数的返回值。
// 函数的定义
void test()
{
}
int main()
{
// 函数的调用
test(); // right
void test(); // error, void关键字只能出现在定义,不可能出现在调用的地方
int a = test(); // error, 函数定义根本就没有返回值
return 0;
}
b)如果函数定义有返回值,这个返回值我们根据用户需要可用可不用,但是,假如我们需要使用这个函数返回值,我们需要定义一个匹配类型的变量来接收。
// 函数的定义, 返回值为int类型
int test()
{
}
int main()
{
// 函数的调用
int a = test(); // right, a为int类型
int b;
b = test(); // right, 和上面等级
char *p = test(); // 虽然调用成功没有意义, p为char *, 函数返回值为int, 类型不匹配
// error, 必须定义一个匹配类型的变量来接收返回值
// int只是类型,没有定义变量
int = test();
// error, 必须定义一个匹配类型的变量来接收返回值
// int只是类型,没有定义变量
int test();
return 0;
}
4. 函数的声明
如果使用用户自己定义的函数,而该函数与调用它的函数(即主调函数)不在同一文件中,或者函数定义的位置在主调函数之后,则必须在调用此函数之前对被调用的函数作声明。
所谓函数声明,就是在函数尚在未定义的情况下,事先将该函数的有关信息通知编译系统,相当于告诉编译器,函数在后面定义,以便使编译能正常进行。
注意:一个函数只能被定义一次,但可以声明多次。
#include <stdio.h>
int max(int x, int y); // 函数的声明,分号不能省略
// int max(int, int); // 另一种方式
int main()
{
int a = 10, b = 25, num_max = 0;
num_max = max(a, b); // 函数的调用
printf("num_max = %d\n", num_max);
return 0;
}
// 函数的定义
int max(int x, int y)
{
return x > y ? x : y;
}
函数定义和声明的区别:
1)定义是指对函数功能的确立,包括指定函数名、函数类型、形参及其类型、函数体等,它是一个完整的、独立的函数单位。
2)声明的作用则是把函数的名字、函数类型以及形参的个数、类型和顺序(注意,不包括函数体)通知编译系统,以便在对包含函数调用的语句进行编译时,据此对其进行对照检查(例如函数名是否正确,实参与形参的类型和个数是否一致)。
5. main函数与exit函数
在 main 函数中调用 exit 和 return 结果是一样的,但在子函数中调用 return 只是代表子函数终止了,在子函数中调用 exit,那么程序终止。
#include <stdio.h>
#include <stdlib.h>
void fun()
{
printf("fun\n");
//return;
exit(0);
}
int main()
{
fun();
while (1);
return 0;
}
6. 多文件(分文件)编程
- 分文件编程
- 把函数声明放在头文件xxx.h中,在主函数中包含相应头文件
- 在头文件对应的xxx.c中实现xxx.h声明的函数
2. 防止头文件重复包含
当一个项目比较大时,往往都是分文件,这时候有可能不小心把同一个头文件 include 多次,或者头文件嵌套包含。
a.h 中包含 b.h :
#include "b.h"
b.h 中包含 a.h :
#include "a.h"
main.c 中使用其中头文件:
#include "a.h"
int main()
{
return 0;
}
编译上面的例子,会出现如下错误:
为了避免同一个文件被include多次,C/C++中有两种方式,一种是 #ifndef 方式,一种是 #pragma once 方式。
方法一:
#ifndef __SOMEFILE_H__
#define __SOMEFILE_H__
// 声明语句
#endif
方法二:
#pragma once
// 声明语句
4. 内存管理
4.1. 作用域
C语言变量的作用域分为:
- 代码块作用域(代码块是{}之间的一段代码)
- 函数作用域
- 文件作用域
1. 局部变量
局部变量也叫 auto 自动变量( auto 可写可不写),一般情况下代码块{ }内部定义的变量都是自动变量,它有如下特点:
- 在一个函数内定义,只在函数范围内有效
- 在复合语句中定义,只在复合语句中有效
- 随着函数调用的结束或复合语句的结束局部变量的声明声明周期也结束
- 如果没有赋初值,内容为随机
#include <stdio.h>
void test()
{
//auto写不写是一样的
//auto只能出现在{}内部
auto int b = 10;
}
int main(void)
{
//b = 100; //err, 在main作用域中没有b
if (1)
{
//在复合语句中定义,只在复合语句中有效
int a = 10;
printf("a = %d\n", a);
}
//a = 10; //err离开if()的复合语句,a已经不存在
return 0;
}
2. 静态(static)局部变量
- static局部变量的作用域也是在定义的函数内有效
- static局部变量的生命周期和程序运行周期一样,同时staitc局部变量的值只初始化一次,但可以赋值多次
- static局部变量若未赋以初值,则由系统自动赋值:数值型变量自动赋初值0,字符型变量赋空字符
#include <stdio.h>
void fun1()
{
int i = 0;
i++;
printf("i = %d\n", i);
}
void fun2()
{
//静态局部变量,没有赋值,系统赋值为0,而且只会初始化一次
static int a;
a++;
printf("a = %d\n", a);
}
int main(void)
{
fun1();
fun1();
fun2();
fun2();
return 0;
}
输出结果:
3. 全局变量
- 在函数外定义,可被本文件及其它文件中的函数所共用,若其它文件中的函数调用此变量,须用 extern 声明
- 全局变量的生命周期和程序运行周期一样
- 不同文件的全局变量不可重名
4. 静态(static)全局变量
- 在函数外定义,作用范围被限制在所定义的文件中
- 不同文件静态全局变量可以重名,但作用域不冲突
- static 全局变量的生命周期和程序运行周期一样,同时 staitc 全局变量的值只初始化一次
5. extern全局变量声明
extern int a; 声明一个变量,这个变量在别的文件中已经定义了,这里只是声明,而不是定义。
6. 全局函数和静态函数
在 C 语言中函数默认都是全局的,使用关键字 static 可以将函数声明为静态,函数定义为 static 就意味着这个函数只能在定义这个函数的文件中使用,在其他文件中不能调用,即使在其他文件中声明这个函数都没用。
对于不同文件中的 staitc 函数名字可以相同。
注意:
- 允许在不同的函数中使用相同的变量名,它们代表不同的对象,分配不同的单元,互不干扰。
- 同一源文件中,允许全局变量和局部变量同名,在局部变量的作用域内,全局变量不起作用。
- 所有的函数默认都是全局的,意味着所有的函数都不能重名,但如果是staitc函数,那么作用域是文件级的,所以不同的文件static函数名是可以相同的。
7. 总结
类型 | 作用域 | 生命周期 |
---|---|---|
auto变量 | 一对{ }内 | 当前函数 |
static局部变量 | 一对{ }内 | 整个程序运行期 |
extern变量 | 整个程序 | 整个程序运行期 |
static全局变量 | 当前文件 | 整个程序运行期 |
extern函数 | 整个程序 | 整个程序运行期 |
static函数 | 当前文件 | 整个程序运行期 |
register变量 | 一对{ }内 | 当前函数 |
4.2. 内存布局
1. 内存分区
C 代码经过预处理、编译、汇编、链接4步后生成一个可执行程序。
在 Linux 下,程序是一个普通的可执行文件,以下列出一个二进制可执行文件的基本情况:
通过上图可以得知,在没有运行程序前,也就是说程序没有加载到内存前,可执行程序内部已经分好3段信息,分别为代码区(text)、数据区(data)和未初始化数据区(bss)3 个部分(有些人直接把data和bss合起来叫做静态区或全局区)。
-
代码区
存放 CPU 执行的机器指令。通常代码区是可共享的(即另外的执行程序可以调用它),使其可共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。代码区通常是只读的,使其只读的原因是防止程序意外地修改了它的指令。另外,代码区还规划了局部变量的相关信息。 -
全局初始化数据区/静态数据区(data段)
该区包含了在程序中明确被初始化的全局变量、已经初始化的静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量)。 -
未初始化数据区(又叫 bss 区)
存入的是全局未初始化变量和未初始化静态变量。未初始化数据区的数据在程序开始执行之前被内核初始化为 0 或者空(NULL)。
程序在加载到内存前,代码区和全局区(data和bss)的大小就是固定的,程序运行期间不能改变。然后,运行可执行程序,系统把程序加载到内存,除了根据可执行程序的信息分出代码区(text)、数据区(data)和未初始化数据区(bss)之外,还额外增加了栈区、堆区。
-
代码区(text segment)
加载的是可执行文件代码段,所有的可执行代码都加载到代码区,这块内存是不可以在运行期间修改的。 -
未初始化数据区(BSS)
加载的是可执行文件BSS段,位置可以分开亦可以紧靠数据段,存储于数据段的数据(全局未初始化,静态未初始化数据)的生存周期为整个程序运行过程。包含:未初始化的静态局部变量(默认初始值为0)、未初始化的静态全局变量(默认初始值为0)、未初始化的全局变量(默认初始值为0)。 -
全局初始化数据区/静态数据区(data segment)
加载的是可执行文件数据段,存储于数据段(全局初始化,静态初始化数据,文字常量(只读))的数据的生存周期为整个程序运行过程。包含:初始化的静态局部变量、初始化的静态全局变量、初始化的全局变量。 -
栈区(stack)
栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间。包含:常量、局部变量、数组、结构体、指针、枚举、函数形参。栈区大小:一般windowns为1~8 M,linux为1~16 M。 -
堆区(heap)
堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于BSS区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。包含:音频文件、视频文件、图像文件、文本文件、大的数据。
2. 存储类型总结
类型 | 作用域 | 生命周期 | 存储位置 |
---|---|---|---|
auto变量 | 一对{ }内 | 当前函数 | 栈区 |
static局部变量 | 一对{}内 | 整个程序运行期 | 初始化在data段,未初始化在BSS段 |
extern变量 | 整个程序 | 整个程序运行期 | 初始化在data段,未初始化在BSS段 |
static全局变量 | 当前文件 | 整个程序运行期 | 初始化在data段,未初始化在BSS段 |
extern函数 | 整个程序 | 整个程序运行期 | 代码区 |
static函数 | 当前文件 | 整个程序运行期 | 代码区 |
register变量 | 一对{ }内 | 当前函数 | 运行时存储在CPU寄存器 |
字符串常量 | 当前文件 | 整个程序运行期 | data段 |
3. 堆区内存分配和释放
1) malloc( )
2) free( )
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main()
{
int count, *array, n;
printf("请输入要申请数组的个数:\n");
scanf("%d", &n);
array = (int *)malloc(n * sizeof (int));
if (array == NULL)
{
printf("申请空间失败!\n");
return -1;
}
//将申请到空间清0
memset(array, 0, sizeof(int)*n);
for (count = 0; count < n; count++) /*给数组赋值*/
array[count] = count;
for (count = 0; count < n; count++) /*打印数组元素*/
printf("%2d", array[count]);
free(array);
return 0;
}