当前位置:网站首页>C 语言基础
C 语言基础
2022-07-01 14:13:00 【A-L-Kun】
C 语言语法
一、 基础
1、 第一个程序
// 导入一个文件,std是一个标准库,io是输入输出
/* <> 表示导入系统文件 "" 表示导入自定义的文件 */
#include <stdio.h>
#include <stdlib.h> // 里面包含了system函数
int main() {
printf("Hello, World!\n"); // 打印输出hello world,行注释
system("pause"); // 按任意键继续
/* 块注释 */
return 0; // 表示函数的返回值
}
system:使用系统命令是,成功返回0
2、 程序编译步骤
C 代码编译成可执行程序经过4步:
(1)预处理:宏定义展开、头文件展开、条件编译等,同时将代码中的注释删除,这里并不会检查语法
(2)编译:检查语法,将预处理后文件编译生成汇编文件
(3)汇编:将汇编文件生成目标文件(二进制文件)
(4)链接:C语言写的程序是需要依赖各种库的,所以编译之后还需要把库链接到最终的可执行程序中去
使用gcc查看编译过程
gcc -E hello.c -o hello.i # 预处理
gcc -S hello.i -o hello.s # 编译
gcc -c hello.s -o hello.o # 汇编
gcc hello.o -o hello.exe # 链接
:<<!
# 一步编译的代码:
!
gcc -o hello.exe h1.c h2.c ... # 后面可以编译多个代码,同时生成一个hello.exe 文件
-o:表示输出文件的地址
3、 汇编语言
mov 移动
add 添加
push 入栈
pop 出栈
call 调用
eax 32位寄存器
二、 数据类型
1、 常量与变量
关键字:C语言里面有32个关键字
数据类型

常量
- 在程序运行过程中,其值不能被改变的量
- 常量一般出现在表达式或赋值语句中
定义方式
#define MAX 20 const int max_ = 23; // 不安全变量
- 在程序运行过程中,其值可以发生改变的量
- 变量在使用时必须先定义,定义变量前必须有相应的数据类型
命名规则
- 标识符不能是关键字
- 标识符只能由字母、数字、下划线组成
- 第一个字符必须为字母或下划线
- 标识符中字符区分大小写
变量特点
- 变量在编译时为其分配相应的内存空间
- 可以通过其名字和地址访问相应的内存
定义方式
int max_ = 23; // 其是可以改变的
2、 整型
2.1 格式化输出
| 占位符 | 含义 |
|---|---|
%d | 输出一个有符号的十进制整型数据 |
%o | 输出八进制的整型数据 |
%x | 输出十六进制的整型数据,字母以小写输出 |
%X | 输出十六进制的整型数据,字母以大写输出 |
%u | 输出一个十进制的无符号数 |
2.2 定义
// 无符号 unsigned;有符号 signed
int a = -10; // 有符号代表有正负,默认为有符号
unsigned int b = 10; // 无符号数,只能为正数,计算结果也要为正数
int c = 0123; // 定义八进制数据,以 0 开头
int d = 0x234ba; // 定义十六进制数据,以 0x 开头
C 不能直接书写二进制数据的形式
例如:
#include <stdio.h>
int main() {
int a;
scanf_s("%d", &a); // 通过键盘输入赋值
printf_s("%d \n", a); // 输出值
return 0;
}
使用
scanf会出现安全问题,使用scanf_s,安全输入
使用不同的关键字定义整型,其开辟的空间是不一样的,所占字节数与所选择的操作系统有关,可以使用sizeof(int)来查看所占空间(BYTE)
3、 字符型
3.1 定义
字符型变量用于存储一个单一字符,在C 语言中用char表示,其中每个字符变量都会占用1个字节。在给字符型变量赋值时,需要使用单引号
字符型变量实际上并不是把该字符本身放到变量的内存单元中去,而是将该字符对应的acsii编码放到变量的存储单元中。char的本质就是一个1字节大小的整型
特殊字符:
- 转义字符
char a = 'a';
3.2 输入
#include <stdio.h>
int main() {
char a;
scanf_s("%c", &a); // 安全输入
printf_s("%c \n", a); // 安全输出
return 0;
}
4、 浮点型
浮点型变量也可以称为实型变量,浮点型变量是用来存储小数值的。在 C 语言中,浮点型变量分为两种:单精度浮点数(float)、双精度浮点数(double),但是double型变量所表示的浮点数比float型变量更准确
由于浮点型变量是由有限的存储单元组成的,因此只能提供有限的有效数字。在有效位以外的数字将被舍去,这样可能会产生一些误差。
不以f结尾的常量是double类型,以f结尾的常量(如:3.14f)是float类型
5、 类型限定符
| 限定符 | 含义 |
|---|---|
extern | 声明一个变量,extern声明的变量没有建立存储空间 |
const | 定义一个常量,常量的值不能修改 |
Volatile | 防止编译器优化代码 |
register | 定义寄存器变量,提高效率 |
6、 字符串
6.1 字符串常量
- 字符串是内存中一段连续的
char空间,以'\0'结尾 - 字符串常量是有双引号括起来的字符序列
字符串常量和字符常量不同
- 每个字符串的结尾,编译器会自动添加一个结束标志位
'\0'
定义
char* a = "hello"; // 使用指针来定义
char b[] = "hello"; // 使用字符数组来定义
printf("%s\n", a); // 输出字符串
6.3 printf 和 putchar
printf是输出一个字符串,putchar输出一个字符
6.3.1 printf
占位符:
| 格式 | 含义 |
|---|---|
| %a,%A | 浮点数、十六进制数字和p-计数法 |
| %c | 一个字符 |
| %C | 一个ISO宽字符 |
| %d | 有符号十进制整数(int)(%ld ,%Ld为:长整型数据,%hd:短整型数) |
| %e,%E | 浮点数,e-计数法,E-计数法 |
| %f | 单精度浮点数 |
| %g,%G | 根据数值不同自动选择%f或%e |
| %i | 有符号十进制数(与%d相同) |
| %o | 无符号八进制整数 |
| %p | 指针 |
| %s | 对应字符串char*(%s = %hs = %hS 输出 窄字符) |
| %S | 对应宽字符串WCAHR*(%ws = %S 输出宽字符串) |
| %u | 无符号十进制整数(unsigned int) |
| %x,%X | 使用十六进制数字0xf的无符号十六进制整 |
| %% | 打印一个%号 |
| %I64d | 用于int64 或者 long long |
| %I64u | 用于uint64 或者unsigned long long |
| %I64x | 用于64 位16进制数字 |
附加格式:
| 符号 | 含义 |
|---|---|
| - | 左对齐 |
| + | 右对齐 |
| .n | 对于小数点,保留n位小数 |
| # | 对c,s,d,u无影响,对o类输出前加缀为o,对x类,在输出前缀加0x,对e,g,f当结果有小数时给出小数点 |
| m | 代表数据的最小宽度 |
6.3.2 putchar
char ch = '0';
putchar(ch); // 输出一个字符
putchar('\n');
putchar(97);
输出字符可以是变量、字符、数字或者转义字符
6.4 scanf_s 和 getchar
getchar是从标准输入设备读取一个字符;scanf_s通过%转义的方式可以得到用户通过标准输入设备输入的数据
6.4.1 scanf_s
int a, b;
scanf_s("%d %d", &a, &b); // 输入两个整型数据,使用空格(分隔符)或换行
6.4.2 getchar
char ch;
ch = getchar(); // 只会接收一个字符,如果有其他的,可以使用while循环遍历,也可以用来作为暂时停留界面
putchar(ch);
7、 类型转换
数据有不同的类型,不同类型数据之间进行混合运算时必然涉及到类型的转换问题
7.1 隐式转换
自动转换:遵循一定的规则,由编译系统自动完成
char a = 2;
int c = a; // a 变成了int类型,有符号数会转换成无符号数
小范围的类型会自动转换成大范围的类型运算

7.2 显式转换
把表达式的运算结果强制转换成所需的数据类型
数据类型2 变量2 = (数据类型2)变量1 // 其不会四舍五入,直接丢失后面的数据
其会造成精度的丢失
三、 运算符
1、 算术运算符
用于处理四则运算
+:加 -:减 *:乘 /:除 %:取余 ++:自增 --:自减
// 前自增先赋值,后运算;后自增相反
2、 赋值运算符
用于将表达式的值赋给变量
+= -= /= *= =
3、 比较运算符
用于表达式的比较,并返回一个真值(true)或假值(false)
== != > < >= <=
4、 逻辑运算符
用于根据表达式的值返回真值或假值
!:非 &&:与 ||:或
所有非零的值都是真值,非真即假
四、 流程结构
C/C++支持最基本的三种程序运行结构:顺序结构、选择结构、循环结构
- 顺序结构:程序按顺序执行,不发生跳转
- 选择结构:依据条件是否满足,有选择的执行相应功能
- 循环结构:依据条件是否满足,循环多次执行某段代码
1、选择结构
1.1 if语句
作用:执行满足条件的语句
if语句的三种形式
- 单行格式if语句
- 多行格式if语句
- 多条件的if语句
1.1.2 单行格式
#include <stdio.h>
int main() {
// 选择结构 单行if语句
// 用户输入一个数字
int num = 0;
printf_s("请输入一个数字:");
scanf_s("%d", &num);
// 判断数字是否大于100,是则输出原数
if (num >= 100){ // 注意if语句后面不要加分号,否则,if将不会进行判断
printf_s("%d\n", num);
}
return 0;
}
1.1.3 多行if语句
#include <stdio.h>
int main() {
// 选择结构 单行if语句
// 用户输入一个数字
int num = 1;
printf_s("请输入一个数字:");
scanf_s("%d", num);
// 判断数字是否大于100,是则输出原数;否则,输出0
if (num >= 100) { // 注意if语句后面不要加分号,否则,if将不会进行判断
printf_s("%d", num);
}
else {
printf_s("%d", 0);
}
return 0;
}
1.1.4 多条件if语句
#include <stdio.h>
int main() {
// 选择结构 单行if语句
// 用户输入一个数字
int num = 1;
printf_s("请输入一个数字:");
scanf_s("%d", num);
// 判断数字是否大于100,是则输出原数;否则,输出0
if (num >= 600) { // 注意if语句后面不要加分号,否则,if将不会进行判断
printf_s("%d", 600);
}
else if (num >= 100) { // 注意if语句后面不要加分号,否则,if将不会进行判断
printf_s("%d", num);
}
else {
printf_s("%d", 0);
}
return 0;
}
if ( 条件1 ) { 条件1满足执行语句} else if ( 条件2 ) { 条件2满足,同时条件1不满足,执行的语句 }··· else { 都不满足执行的语句 }
1.1.5 嵌套if语句
在if语句中,可以嵌套使用if语句,达到更加精确的条件判断
案例:输入3个数字,判断出最大的数字
#include <stdio.h>
int main() {
int num = 1;
int num1 = 1;
int num2 = 1;
printf_s("请输入三个数字:");
scanf_s("%d,%d,%d", &num, &num1, &num2);
if ( num > num1 ) { // 判断 num 和 num1
if ( num > num2 ) { // 判断 num 和 num2
printf_s("%d最大\n", num);
}
else {
printf_s("%d最大\n", num2);
}
}
else {
if (num1 > num2) {
printf_s("%d最大\n", num1);
}
else {
printf_s("%d最大", num2);
}
printf_s("判断完成");
}
return 0;
}
1.2 三目运算符
作用:通过三目运算实现简单的判断
语法:表达式1 ? 表达式2 : 表达式3
#include <stdio.h>
int main() {
// 三目运算
// 将 a 和 b 做比较,将大的值赋值给c
int a = 10;
int b = 20;
int c = 0;
c = a > b ? a : b; // 如果 a 比 b 大,则将a赋值给c
/*
if ( a > b ) {
c = a;
}
else {
c = b
}
*/
return 0;
}
在C++中,三目运算符返回的是变量,可以继续赋值
1.3 switch语句
作用:执行多条件分支语句
语法:
switch ( 表达式 ) {
case 结果1: 执行语句; break; // switch里面不一定每个case都要对应break,break的作用的向外跳出一层
······
default: 执行语句; break; // 不一定需要default判断
}
示例
#include <stdio.h>
int main() {
// switch 语句
// 电影打分
int score = 0;
printf_s("请输入分数:");
scanf_s("%d", &score);
printf_s("您打的分数为:%d\n", score);
switch (score) {
case 10:
printf_s("是经典电影\n");
break; // 退出当前分支,如果没有break,则会继续向下运行
default: // 当所有条件不满足时,执行该语句
printf_s("普通\n");
}
}
缺点:判断的时候,只能是整型或者字符型,不可以是一个区间
优点:结构清晰,执行效率高
注意
- switch语句中表达式类型只能是整型或者字符型
- case里如果没有break,那么程序会一直向下执行
2、 循环结构
2.1 while循环语句
作用:满足循环条件,执行循环语句
语法:while ( 循环条件 ) { 循环语句 }
解释:只要循环条件的结果为真,就执行循环语句
#include <stdio.h>
int main() {
int nu = 0;
// 在屏幕中打印0到9的数字
while ( nu < 10 )
{
printf_s("%d\n",nu);
nu++;
}
}
2.2 do···while循环语句
作用:满足循环条件,执行循环语句
语法:do {循环语句} while (循环条件);
注意:与while的区别在于do…while会先执行一次循环时间,在判断循环条件
#include <stdio.h>
int main() {
// do...while语句
// 在屏幕中输出 0 到 9 这10个数字
int num = 0;
do {
printf_s("%d\n", num);
num++;
}
while (num <= 9);
}
2.3 for循环语句
作用:满足循环条件,执行循环语句
语法:for (起始表达式;条件表达式;末尾循环体) { 循环语句; }
#include <stdio.h>
int main() {
// for循环
// 从数字0打印到9
for (int i = 0; i < 10; i++ ) {
printf_s("%d\n", i);
}
// 也可以
int i = 0;
for (;;) {
if (i >= 10) {
break;
}
printf_s("%d\n", i++);
}
}
2.4 嵌套循环
作用:在循环中在嵌套一层循环,解决一些实际问题
#include <stdio.h>
int main() {
// 嵌套循环
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
printf_s(" * ");
}
printf_s("\n");
}
}
3、 跳转语句
3.1 break语句
作用:用于跳出选择结构或者循环结构
break使用的时机:
- 出现在switch语句中,作用是终止case并跳出switch
- 出现在循环语句中,作用是跳出当前的循环语句
- 出现在嵌套循环中,跳出最近的内层循环语句
3.2 continue语句
作用:在循环语句中,跳过本次循环中余下尚未执行的语句,继续执行下一次循环
#include <stdio.h>
int main() {
for (int i = 0; i <= 10; i++) {
if (i % 2 == 0) {
continue;
}
else {
ptintf_s("%d\t", i);
}
}
}
3.3 goto语句
作用:可以无条件跳转语句
语法:goto 标记;
解释:如果标记的名称存在,执行到goto语句时,会跳转到标记的位置
#include <stdio.h>
int main() {
// goto
printf_s("hello\n");
printf_s("hello\n");
printf_s("hello\n");
goto flag;
printf_s("hello\n");
printf_s("hello\n");
flag:
printf_s("world\n");
}
五、 数组和字符串
1、 概述
在程序设计中,为了方便处理数据把具有相同类型的若干变量按有序形式组织起来——称为数组
数组就是在内存中连续的相同类型的变量空间。同一个数组所有的成员都是相同的数据类型,同时所有的成员在内存中的地址是连续的
数组属于构造数据类型
数组名是地址常量
2、 定义数组
int arr[] = {
1, 2, 4, 5, 6, 7 };
int arr1[6] = {
1, 2, 3, 4, 5, 6 };
int len = sizeof(arr) / sizeof(arr[0]); // 获取数组的长度
整型数组默认初始化的值为0,
3、 冒泡排序
作用:最常用的排序算法,对数组内的元素进行排序
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个
- 对每一对相邻元素做同样的工作,执行完毕后,找到第一个最大值
- 重复以上的步骤,每次比较次数-1,直到不需要比较
#include <stdio.h>
int main() {
// 利用冒泡排序,实现升序排序
int arr[ ] = {
4, 2, 8, 0, 5, 7, 1, 3, 9};
printf_s("排序前:");
for (int i = 0; i < (sizeof(arr) / sizeof(arr[0])); i++) {
printf_s("%d\t", arr[i]);
}
for (int i = 0; i < (sizeof(arr) / sizeof(arr[0]) - 1); i++) {
// 排序次数为元素个数减一
for (int j = 0; j < (sizeof(arr) / sizeof(arr[0]) - 1 - i); j++) {
// 内层循环对比次数元素个数 - 排序次数 - 1
if (arr[j] > arr[j + 1]) {
// 如果前一个数字比第二个数字大,交换顺序
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
printf_s("\n排序后:");
for (int i = 0; i < (sizeof(arr) / sizeof(arr[0])); i++) {
printf_s("%d\t", arr[i]);
}
}
4、 二维数组
二维数组定义的一般形式是:
类型说明符 数组名[常量表达式1][常量表达式2];
其中常量表达式1表示行数,常量表达式2表示列数
5、 字符串
char arr[5] = {
'h', 'e', 'l', 'l', 'o' }; // 字符数组
char* a = "world"; // 字符串
如果字符数组有
'\0'的标志,则可以认为其是一个字符串,字符串是字符数组的特例
字符数组和字符串的区别:
- C 语言中没有字符串这种数据类型,可以通过
char的数组来替代 - 字符串一定是一个
char的数组,但char的数组未必是字符串 - 数字0(和字符
'\0'等价)结尾的char数组就是一个字符串,但如果char数组没有以数字0结尾,那么就不是一个字符串,只是普通字符数组,所以字符串是一种特殊的char数组
5.1 字符串的输入
char str[100];
scanf_s("%s", str); // 默认使用空格分隔
// gets_s()
char* gets_s(char* s);
// 功能:从标准输入读取字符,并保存到指定的内存空间,直到出现换行或读到文件结尾为止,成功返回字符串,失败返回空
// fgets_s()
char* fgets(char* s, int size, FILE* stream);
// 功能:从stream指定的文件中读入字符,保存到所指定的内存空间,直到出现换行字符、读到文件结尾或是已读了size-1个字符为止,最后会自动加上'\0',作为字符串结束,成功返回字符串,失败返回空。其通过键盘输入时,包括了最后的'\n'换行
scanf_s和gets_s的区别:
scanf_s不允许输入有空格;gets_s允许输入有空格
fgets里面的stream是文件操作指针
例如,获取键盘输入的字符串:
char ch[101];
sacnf_s("%s", ch);
gets_s(ch);
fgets(ch, sizeof(ch), stdin);
上面的代码都是获取从键盘输入的字符串
5.2 字符串的输出
printf_s("%s\n", str);
int puts(const char* s);
// 功能:标准设备输出字符串,在输出完成后自动输出一个'\n'
int fputs(const char* s, FILE* stream);
// 功能:将s所指定的字符串写入到stream指定的文件中,字符串结束符'\0'不写入文件,同时不会自动换行
例如:
char c[] = "hello world";
puts(c);
fputs(c, stdout);
5.3 字符串长度
#include <string.h>
int strlen(const char* s);
// 功能:计算指定字符串的长度,不包括'\0'
六、 函数
1、 概述
1.1 函数的分类
C 程序是由函数构成的,我们写的代码都是由主函数 main() 开始执行的。函数是 C 程序的基本模块,是用于完成特定任务的程序代码单元。
从函数定义的角度看,函数可分为系统函数和用户定义函数两种:
- 系统函数,即库函数:这是编译系统提供的,用户不必自己定义这些函数,可以直接使用它们
- 用户定义的函数:用以解决用户的专门需要
从函数执行结果的角度来看, 函数可分为有返回值函数和无返回值函数两种
- 有返回值函数:此类函数被调用执行完后将向调用者返回一个执行结果,称为函数返回值。(必须指定返回值类型和使用return关键字返回对应数据)
- 无返回值函数:此类函数用于完成某项特定的处理任务,执行完成后不向调用者返回函数值。(返回值类型为void, 不用使用return关键字返回对应数据)
从主调函数和被调函数之间数据传送的角度看,又可分为无参函数和有参函数两种
- 无参函数:在函数定义及函数说明及函数调用中均不带参数。主调函数和被调函数之间不进行参数传送
- 有参函数:在函数定义及函数说明时都有参数,称为形式参数(简称为形参)。在函数调用时也必须给出参数,称为实际参数(简称为实参)
1.2 函数的作用
- 函数的使用可以省去重复代码的编写,降低代码重复率
- 函数可以让程序更加模块化,从而有利于程序的阅读,修改和完善
1.3 函数的调用:获得随机数
当调用函数时,需要关心5要素:
- 头文件:包含指定的头文件
- 函数名字:函数名字必须和头文件声明的名字一样
- 功能:需要知道此函数能干嘛后才调用
- 参数:参数类型要匹配
- 返回值:根据需要接收返回值
#include <time.h>
#include <stdlib.h>
#include <stdio.h>
int getRandNum() {
srand((unsigned int)time(NULL)); // 使用随机种子
// srand((size_t)time(NULL));
int n = rand() % 10; // 产生0到9的随机数
// rand() % (max - min + 1) + min 取得 min ~ max 的随机数
return n;
}
2、 函数定义和使用
2.1 函数的定义
定义函数的目的
- 将一个常用的功能封装起来,方便以后调用
自定义函数的书写形式
返回值类型 函数名(参数类型 形参1, 参数类型 形参2, ...) { 函数体; 返回值; } int get_num(int a, int b) { int sum = a + b; return sum; }在函数调用过程中传递的参数称为实参,有具体的值
在函数的定义中参数称为形参,形式参数
在函数调用过程中实参传递给形参
在函数调用结束后,函数会在内存中销毁
2.2 具体介绍
2.2.1 函数名
理论上,函数名是可以随意起名字,见名知义,应该让用户看到这个函数名字就知道这个函数的功能。注意,函数名的后面要加括号,代表这个是函数,而不是普通的变量
2.2.2 形参列表
在定义函数时,指定形参,在未出现函数调用时,它们并不占内存中的存储单元。因此,称它们是形式参数或虚拟参数,简称形参,表示它们并不是实际存在的数据,所以,形参里的变量不能赋值
int sum(int a, int b = 20); // 这会报错,形参在C语言里面不能赋值
在定义函数时指定的形参,必须是类型+变量的形式
int sum(int a, int b); // right
2.2.3 函数体
花括号里面的内容即为函数体的内容,这里为函数功能实现的过程,这和以前的写代码没太大区别,以前我们把代码写在main()函数里,现在只是把这个写到别的函数里面
2.2.4 返回值
函数的返回值是通过函数中的return语句获得的,return后面的值也可以是一个表达式
- 尽量保证
return语句中表达式的值和函数返回类型是同一类型 - 如果函数返回的类型和
return语句中表达式的值不一致,则以函数返回类型为准,即函数返回类型决定返回值类型。对数值型数据,可以自动进行类型转换。
2.3 函数调用
定义函数后,我们需要调用此函数才能执行到这个函数里面的代码段。这和main()函数不一样,main()为编译器设定好自动调用的主函数,无需人为调用,我们都是在main()函数里调用别的函数,一个C程序里有且只有一个main()函数
2.3.1 函数执行流程
- 进入
main函数 - 调用
test函数- 它会在
main()函数的前面寻找有没有一个名字叫做test的函数定义 - 如果找到,接着检查函数的参数,这里调用函数是没有传参,函数定义也没有形参,参数类型匹配
- 开始执行
test()函数,这时候,main()函数李曼的执行会阻停在test()这一行代码,等待函数执行完成
- 它会在
- 函数执行完成后,
main()函数继续执行
2.3.2 形参和实参
- 形参出现在函数定义中,在整个函数体内都可以使用,离开该函数则不能使用
- 实参出现在主调用函数中,进入被调函数后,实参也不能使用
- 实参变量对形参变量的数据传递是"值传递",即单向传递,只由实参传给形参,而不能由形参传回来给实参
- 在调用函数时,编译系统临时给形参分配存储单元。调用结束后,形参单元被释放
- 实参单元与形参单元是不同的单元。调用结束后,形参单元被释放,函数调用结束返回主函数后则不能再使用该形参变量。实参单元仍保留并维持原值。因此,在执行一个被调用函数时,形参的值如果发生改变,并不会改变主调函数中实参的值
2.4 函数声明
#include <stdlib.h>
#include <stdio.h>
int getSum(int, int); // extern int getSum(int, int);
int main() {
int sum = getSum(1, 2);
printf_s("%d\n", sum);
system("pause");
return 0;
}
int getSum(int a, int b) {
return a + b;
}
从广义的角度来讲,声明中包含着定义,即定义是声明的一个特例,所以并非所有的声明都是定义:
int b它既是声明,同时又是定义- 对于
extern b来讲,它只是声明,不是定义
一般的情况下,把建立存储空间的声明称之为“定义”,而把不需要建立存储空间的声明称之为“声明”
4、 多文件编程
一个文件声明函数
如,创建一个hello.h
#ifndef NEW_HELLO_H // 如果没有这个文件,可以防止头文件包含
// 也可以使用 #pragma once 来防止头文件包含只能在Windows中使用
#define NEW_HELLO_H
int getSum(int, int); // 可以把extern省略
#endif //NEW_HELLO_H
一个文件实现函数hello.c
#include "hello.h"
int getSum(int a, int b) {
return a + b;
}
一个文件调用函数main.c
#include <stdlib.h>
#include <stdio.h>
#include "hello.h"
int main() {
int sum = getSum(1, 2);
printf_s("%d\n", sum);
system("pause");
return 0;
}
编译代码
gcc -o hello.exe main.c hello.c hello.h
5、 main函数
int main(int argc, const char * argv[]) {
printf_s("命令行传入参数的个数:%d,第一个参数为:%s\n", argc, argv[0]);
system("pause");
return 0;
}
main的含义:
- main是函数的名称,和我们自定义的函数名称一样,也是一个标识符
- 只不过main这个名称比较特殊,程序一启动就会自动调用它
return 0的含义:
- 告诉系统
main函数是否正确的被执行了 - 如果
main函数的执行正常,那么就返回0 - 如果
main函数的执行不正常,那么就返回一个非0的函数
返回值类型
- 一个函数
return后面写的是什么类型,函数的返回值类型就必须是什么类型,所以写int
形参列表的含义:
int argc- 系统在启动程序是调用
main函数时传递给argv的值的个数
- 系统在启动程序是调用
const char* argv[]- 系统在启动程序时传入的值,默认情况下系统会传入一个值,这个值就是
main函数执行文件的路径 - 也可以通过命令行或项目设置传入其它参数
- 系统在启动程序时传入的值,默认情况下系统会传入一个值,这个值就是
6、 递归函数
什么是递归函数?
- 一个函数在它的函数体内调用它自身称为递归调用
递归函数构成条件
- 自己调用自己
- 存在一个条件能够让递归结束
- 问题的规模能够缩小
实例:求一个数的累加和
#include <stdlib.h>
#include <stdio.h>
int getSum(int n) {
return n == 1 ? 1 : getSum(n - 1) + n;
}
int main() {
int n;
scanf_s("%d", &n);
printf_s("累加和为:%d\n", getSum(n));
system("pause");
return 0;
}
递归和循环区别
- 能用循环实现的功能,用递归都可以实现
- 递归常用于"回溯", “树的遍历”,"图的搜索"等问题
- 但代码理解难度大,内存消耗大(易导致栈溢出), 所以考虑到代码理解难度和内存消耗问题, 在企业开发中一般能用循环都不会使用递归
七、 指针
1、 概述
1.1 内存
内存含义:
- 存储器:计算机的组成,用来存储程序和数据,辅助CPU进行运算处理的重要部分
- 内存:内部存储器,暂存程序/数据——掉电丢失
- 外存:外部存储器,长时间保存程序/数据——掉电不丢失
内存是沟通CPU与硬盘的桥梁:
- 暂存放CPU中的运算数据
- 暂存与硬盘等外部存储器交换的数据
1.2 物理存储器和存储地址空间
有关内存的两个概念:物理存储器和存储地址空间
物理存储器:实际存在的具体存储芯片
- 主板上装插的内存条
- 显示卡上的显示RAM芯片
- 各种适配卡上的RAM芯片和ROM芯片
存储地址空间:对存储器编码的范围。我们在软件上常说的内存是指这一层含义
- 编码:对每个物理存储单元分配一个号码
- 寻址:可以根据分配的号码找到相应的存储单元,完成数据的读写
1.3 内存地址
- 将内层抽象成一个很大的一维字符数组
- 编码就是对内存的每一个字节分配成一个32位或64位的编号
- 这个内存编号我们称之为内存地址
内存中的每一个数据都会分配相应的地址:
char:占一个字节分配一个地址int:占四个字节分配四个地址
1.4 指针和指针变量
C 语言中把地址形象地称作指针
获取地址的方法:
int a = 10;
printf("%x\n", &a); // & 为取址运算符
可以保存地址值(指针)的变量称为指针变量,因为指针变量中保存的是地址值,故可以把指针变量形象地比喻成地址箱
2、 指针基础知识
2.1 指针变量的定义和使用
- 指针也是一种数据类型,指针变量也是一种变量
- 指针变量指向谁,就把谁的地址赋值给指针变量
// 定义一个指针变量
int* p;
int a = 10;
// 给指针变量赋值
p = &a;
2.2 指针变量间接修改变量的值
使用取值运算符来修改指针变量所对应的值
printf("修改前:%d", a);
*p = 100;
printf("修改后:%d", a);
2.3 指针大小
- 使用
sizeof()测量指针的大小,得到的总是:4或8 sizeof()测的是指针变量指向内存地址的大小- 在32位平台,所有指针地址都是32位(4bit)
- 在64位平台,所有指针地址都是64位(8bit)
printf("%d\n", sizeof(int*));
2.4 野指针和空指针
指针变量也是变量,是变量就可以任意赋值,不要越界即可,但是任何数值赋值给指针变量没有意义,因为这样的指针就成了野指针,此指针指向的区域是未知的(操作系统不允许此指针指向内存区域)。所以,也指针不会直接引发错误,操作指针指向的内存区域才会出问题。
int* p = 100; // 野指针 -> 指针变量指未知的空间
// 操作系统将0~255作为系统占用空间,不允许访问操作
// 操作野指针对应的空间可能报错
printf("%d\n", *p);
但是,野指针和有效指针变量保存的都是数值,为了标志此指针变量没有指向任何变量。C语言中,可以把NULL赋值给此指针,这样就标志此指针为空指针,没有任何指针。
int* p = NULL; // 空指针是指内存地址编号为0的内存空间
2.5 万能指针
void*指针可以指向任意变量的内存空间:
int a = 10;
// 万能指针可以接收任意类型变量的内存地址
void* p = &a;
// *p = 100; // 报错了,非法的间接寻址
// 在通过万能指针修改变量的值时,需要找到变量对应的指针类型
*(int*)p = 100;
printf_s("%d\n", a);
2.6 const修饰指针变量
int a = 100;
int b = 200;
// 常量指针
// 修饰 *,指针指向内存区域不能修改,指针指向可以改变
const int* p1 = &a;
// *p1 = 2;
p1 = &b;
// 指针常量
// 修饰 p2,指针指向不能改变,指针指向的内存可以修改
int* const p2 = &a;
// p2 = &b;
*p2 = 2;
3、 指针和数组
3.1 数组指针
指针操作数组
数组名字是数组的首元素地址,但它是一个常量
int arr[] = {
1, 2, 3};
// arr = 10; // 其报错,因为a相当于一个指针常量,其指向不能改变
// 数组名是数组第一个元素的首地址
int* p = arr; // p 和 arr 等价,指向数组的指针,数组指针
for (int i = 0; i < 3; ++i) {
printf_s("%d\n", *p++); // 使用指针偏移
// printf_s("%d\n", *(p+i)); // 相当于索引取值
// printf_s("%d\n", p[i]); // 索引取值
}
int step = p - arr; // 两指针相减,得到的是指针偏移的步长
printf_s("%d\", step);
数组作为函数参数会退化为指针,丢失了数组的精度
指针操作数组时,下标允许是负数
3.2 指针加减运算
- 指针计算不是简单的整数相加
- 如果是一个
int*,+1的结果是增加一个int的大小 - 如果是一个
char*,+1的结果是增加一个char的大小
#include <stdio.h>
void my_strcpy1(char* dest, char* src) {
int i = 0;
while (*(src+i)) {
*(dest+i) = *(src+i);
i++;
}
*(dest+i) = 0;
}
void my_strcpy2(char* dest, char* src) {
// 纯指针偏移
while (*src) {
*dest++ = *src++;
}
*dest = 0;
}
void my_strcpy3(char* dest, char* src) {
// 纯指针偏移
while (*dest++ = *src++);
}
int main() {
char str_[] = "hello world";
char dest[100];
my_strcpy3(dest, str_);
printf_s("%s\n", dest);
return 0;
}
两个指针进行运算会变成野指针,其为没有意义的操作(两数组指针相减,其为偏移量);但是可以进行比较运算
3.3 指针数组
指针数组,它是数组,数组的每个元素都是指针类型
int a = 1;
int b = 2;
int c = 3;
int* d = &a;
int* e = &b;
int* f = &c;
int* arr[] = {
d, e, f}; // 指针数组,存储指针的数组
指针数组里面也可以存储数组,指针数组是一个特殊的二维数组模型
#include <stdio.h>
int main() {
int a[] = {
1, 2, 3};
int b[] = {
4, 5, 6};
int c[] = {
7, 8, 9};
int* d[] = {
&a, &b, &c}; // int** d = int* d[];
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
// 访问指针数组里面的内容
// printf_s("%d\t", d[i][j]);
// printf_s("%d\t", *(d[i] + j));
// printf_s("%d\t", *(*(d+i) + j));
}
puts("");
}
return 0;
}
4、 二级指针
二级指针相当于指针数组
二级指针加偏移量相当于跳过了一维数组的大小
#include <stdio.h>
int main() {
int a[] = {
1, 2, 3};
int b[] = {
4, 5, 6};
int c[] = {
7, 8, 9};
int* d[] = {
&a, &b, &c};
int** p = d;
// 二级指针加偏移量,相当于跳过了一个一维数组
printf_s("%d", **(p + 1));
// 一级指针加偏移量,相当于跳过了一个元素
printf_s("%d", *(*p + 1));
return 0;
}
5、 指针和函数
5.1 地址传递
#include <stdio.h>
void test(int* a) {
*a = 20;
}
int main() {
int a = 10;
printf_s("调用函数前:%d\n", a);
test(&a);
printf_s("调用函数后:%d\n", a);
return 0;
}
5.2 数组名做函数参数
数组名做函数参数,函数的形参会退化成指针
#include <stdio.h>
#ifndef bool
typedef int bool;
#define true 1
#define false 0
#endif
void bubble(int* arr, int len){
// 升序排序
for (int i = 0; i < len; ++i) {
for (int j = 0; j < len - i - 1; ++j) {
if (arr[j] > arr[j+1]) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
int main() {
int a[] = {
4, 1, 5, 9, 6, 8, 7};
bubble(a, 7);
for (int i = 0; i < 7; ++i) {
printf_s("%d\t", *(a + i));
}
puts("");
return 0;
}
5.3 指针函数
指针作为函数返回值
#include <stdio.h>
#ifndef bool
typedef int bool;
#define true 1
#define false 0
#endif
char* my_strchr(char* str_, char ch) {
while (*str_) {
if (*str_ == ch) {
return str_; // 返回切片后的字符数组
}
*str_++;
}
return NULL; // 返回没有找到要开始切片的元素
}
int main() {
char* str_ = "hello world";
char* b = my_strchr(str_, 'l');
printf_s("%s\n", b);
return 0;
}
5.4 函数指针
如果在程序中定义了一个函数,那么在编译时系统就会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址。而且函数名表示的就是这个地址。既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针。
函数指针不可以进行运算
#include <stdio.h>
#ifndef bool
typedef int bool;
#define true 1
#define false 0
#endif
void (*pointer) (); // 定义一个函数指针
void func() {
printf_s("hello");
}
void print_hello(void (*p)()) {
p();
}
int main() {
pointer = func; // 将函数的地址赋值给函数指针
pointer(); // 调用函数
// 也可以将函数指针作为参数传递给函数
print_hello(func);
/* 等价于 void (*p)(); // 定义函数指针 p = func; // 给函数指针赋值 p(); // 调用函数指针 */
return 0;
}
函数指针一般用于传递回调函数上面
6、 指针和字符串
6.1 字符串出现的次数
使用strstr方法,来统计出现的次数
#include <stdio.h>
#include <string.h>
int main() {
char ch[] = "hello world";
char a[] = "l";
char* p = strstr(ch, a); // 返回第一次出现字符串的地址
int count = 0;
while (p) {
// 如果字符串不为空
count++;
p += strlen(a); // 跳过要查找的字符串
p = strstr(p, a); // 再次截取字符串
}
printf_s("%d\n", count);
return 0;
}
6.2 常用函数
char* strcpy(char* dest, char* src);- 功能:将源字符串,拷贝给目标字符串
- 参数:
dest:目标字符串src:源字符串
- 返回值:
- 成功返回目标字符串
- 失败返回NULL
char* strncpy(char* dest, char* src, size_t n);- 功能:将源字符串的前n个字符拷贝给目标字符串
int strcmp(char* s1, char* s2);- 功能:查看两个字符串是否一样
- 参数:
s1:字符串1s2:字符串2
- 返回值:
s1 = s2:返回值等于0s1 > s2:返回值大于0s1 < s2:返回值小于0
int strncmp(char* s1, char* s2, size_t n);- 功能:查看两个字符串的前n个字符是否相同
char* strcat(char* dest, char* src);- 功能:将src字符串连接到dest的尾部,
\0也会追加过去 - 参数:
dest:目标字符串的首地址src:源字符串的首地址
- 返回值:
- 成功返回目标字符串
- 失败返回NULL
- 功能:将src字符串连接到dest的尾部,
char* strncat(char* dest, char* src, size_t n);- 功能:将src字符串前n个字符连接到dest的尾部,
\0也会追加过去
- 功能:将src字符串前n个字符连接到dest的尾部,
int sprintf(char* str, const* format, ...);- 功能:根据参数format字符串来转换格式化数据,然后将结果输出到str指定的空间中,直到出现字符串结束符
\0为止 - 参数:
str:字符串首地址format:字符串格式化,用法和printf()一样
- 返回值:
- 成功返回实际格式化的字符个数
- 失败返回NULL
- 功能:根据参数format字符串来转换格式化数据,然后将结果输出到str指定的空间中,直到出现字符串结束符
char* strchr(const char* s, char c);- 功能:在字符串s中查找字符c出现的位置
- 参数:
s:字符串首地址c:字符
- 返回值:
- 成功返回第一次出现c的地址
- 失败返回NULL
char* strstr(const char* dest, const char* src);- 功能:在字符串dest中查找字符串src第一次出现的位置
- 参数:
dest:原字符串的首地址src:匹配字符串的首地址
- 返回值:
- 成功返回第一次出现字符串的地址
- 失败返回NULL
char* strtok_s(char* str, const char* delim, char** context);- 功能:将字符串以字符串delim分割,返回分割第一个字符串,其余字符串放入context中
- 参数:
str:要分割的字符串delim:分割符context:一个字符串数组的第一个元素的首地址
- 返回值:
- 成功返回分割后的字符串
- 失败返回NULL
int atoi(const char* nptr);- 功能:
atoi()会扫描nptr,跳过前面的空格字符,知道遇到数字或正负号才开始转换,而遇到非数字或字符串结束符,才会结束转换,并将结果返回 - 参数:
nptr:待转换的字符串
- 返回自:
- 成功转换后的整数
- 类似的还有:
atof()/atol()
- 注意:其在
#include <stdlib.h>里面
- 功能:
八、 内存管理
1、 作用域
C语言变量的作用域分别为:
- 代码块作用域
- 函数作用域
- 文件作用域
1.1 局部变量
局部变量也叫auto自动变量,auto可不写,一般情况下代码块内部定义的变量都是自动变量,它有如下特点:
- 在一个函数内定义,只在函数范围内有效
- 在复合语句中定义,只在复合语句中有效
- 随着函数调用的结束或复合语句的结束局部变量的声明,声明周期也结束
- 如果没有赋初值,内容为随机
- 存储在栈区
1.2 全局变量
- 在函数外定义,可被本文件及其它文件中的函数所共用,若其它文件中的函数调用此变量,须用
extern声明,extern int a;这里是声明,而不是定义 - 全局变量的生命周期和程序运行周期一样
- 不同文件的全局变量不可重名
- 生命周期,从程序创建到程序销毁
- 存储在数据区
1.3 静态变量
静态局部变量
static局部变量的作用域也是在定义的函数内有效static局部变量的生命周期和程序运行周期一样,同时static局部变量的值只初始化一次,但可以多次赋值static局部变量若未赋以初值,则由系统自动赋值,数值型变量自动赋初值0,字符串变量赋空字符- 存储在数据区
静态全局变量
- 作用域:定义所在的文件中
- 生命周期:从程序创建到程序销毁
- 存储位置:存储在数据区
1.4 全局函数和静态函数
在C语言中,函数默认都是全局的,使用关键字static可以将函数声明为静态,函数定义为static就意味着这个函数只能在定义这个函数的文件中使用,在其他文件中不能调用,即使在其他文件中声明这个函数也没法使用
对于不同文件中的static函数名字可以相同
注意:
- 允许在不同的函数中使用相同的变量名,它们代表不同的对象,分配不同的单元,互不干扰
- 同一源文件中,允许全局变量和局部变量同名,在局部变量的作用域内,全局变量不起作用
- 所有的函数默认都是全局的,意味着所有的函数都不能重名,但如果是
static函数,那么作用域是文件级的,所以不同的文件static函数名是可以相同的
2、 内存布局
2.1 内存分区
C 代码经过预处理、编译、汇编、链接,4步后生成一个可执行程序
代码区:
- 程序执行二进制码(程序指令)
- 共享、只读
数据区:
- 初始化数据区(data)
- 未初始化数据区(bss)
- 常量区
栈区:
- 系统为每一个程序分配一个临时的空间
- 存储局部变量、函数信息、函数参数、数组
- 栈区大小为:1M;在Windows中可以扩展到10M;在Linux中可以扩展到16M
堆区:
- 存储大数据、图片、音乐、视频
- 手动开辟:
malloc colloc realloc - 手动释放:
free()
程序在加载到内存前,代码区和全局区的大小就是固定的,程序运行期间不能改变。然后,执行可执行程序,系统把程序加载到内存,除了根据可执行程序的信息分出代码区、数据区和未初始化数据区之外,还额外增加了栈区、堆区
2.2 堆空间开辟和释放
#include <stdio.h>
#include <stdlib.h>
int main() {
// 开辟堆空间存储数据
int* p = (int *)malloc(sizeof(int)*1000); // 开辟1000个整型数据的空间
// 使用堆空间
*p = 123;
printf_s("%d", *p);
// 释放堆空间
free(p);
// 将指针置空,防止出现野指针
p = NULL;
return 0;
}
注意:
释放指针要释放同一个指针
#include <stdlib.h> int main() { int* p = (int *)malloc(sizeof(int)*1000); p = 123; // 改变了指针的地址,同时指针偏移也会改变指针的地址,其为无主指针 free(p); // 释放指针会报错 return 0; }
2.3 内存操作函数
#include <string.h>
void* memset(void* s, int c, size_t n);- 功能:将s的内存区域的前n个字节以参数c填入
- 参数:
s:需要操作内存s的首地址c:填充的字符,unsigned char——0 ~ 255n:指定需要设置的大小,单位是字节
- 返回值:s的首地址
void* memcpy(void* dest, void* src, size_t n);- 功能:拷贝src所指的内存内容的前n个字节到dest所指的内存地址上
- 参数:
dest:目标内存首地址src:原内存首地址,注意:首地址不可以重叠n:需要拷贝的字节数
- 返回值:
dest的首地址
memmove()功能用法和memcpy()一样,区别在于:当内存空间重叠时,memmove()仍然能处理,不过执行效率更低void* memcmp(const void* s1, const void* s2, size_t n);- 功能:比较s1和s2所指向内存区域的前n个字节
- 参数:
s1:内存首地址s1s2:内存首地址s2n:需比较的前n个字节
- 返回值:
s1 = s2:返回值等于0s1 > s2:返回值大于0s1 < s2:返回值小于0
边栏推荐
- 算网融合赋能行业转型,移动云点亮数智未来新路标
- QT社团管理系统
- WebSocket(简单体验版)
- 百度上找的期货公司安全吗?期货公司怎么确定正规
- Station B was scolded on the hot search..
- Using CMD to repair and recover virus infected files
- 【剑指 Offer】55 - II. 平衡二叉树
- Kongsong (Xintong Institute) - cloud security capacity building and trend in the digital era
- Go integrates logrus to realize log printing
- Phpcms realizes the direct Alipay payment function of orders
猜你喜欢

被裁三个月,面试到处碰壁,心态已经开始崩了

sqlilabs less-11~12

App自动化测试开元平台Appium-runner

sqlilabs less13

Leetcode(69)——x 的平方根

Enter the top six! Boyun's sales ranking in China's cloud management software market continues to rise

【商业终端仿真解决方案】上海道宁为您带来Georgia介绍、试用、教程

QT learning management system

Using CMD to repair and recover virus infected files

Realize queue with stack and stack with queue (C language \leetcode\u 232+225)
随机推荐
Research Report on the development trend and competitive strategy of the global high temperature label industry
Build your own website (21)
8 best practices to protect your IAC security!
Research Report on the development trend and competitive strategy of the global indexable milling cutter industry
Basis of target detection (NMS)
【IoT毕设.上】STM32+机智云AIoT+实验室安全监控系统
队列的基本操作(C语言实现)
What "hard core innovations" does Intel have in the first half of 2022? Just look at this picture!
自定义注解实现验证信息的功能
[commercial terminal simulation solution] Shanghai daoning brings you Georgia introduction, trial and tutorial
Liu Dui (fire line safety) - risk discovery in cloudy environment
Six years of technology iteration, challenges and exploration of Alibaba's globalization and compliance
Realize queue with stack and stack with queue (C language \leetcode\u 232+225)
Why did you win the first Taosi culture award of 20000 RMB if you are neither a top R & D expert nor a sales Daniel?
[Jianzhi offer] 54 The k-th node of binary search tree
清华章毓晋老师新书:2D视觉系统和图像技术(文末送5本)
用对场景,事半功倍!TDengine 的窗口查询功能及使用场景全介绍
[NLP] pre training model - gpt1
深度合作 | 涛思数据携手长虹佳华为中国区客户提供 TDengine 强大企业级产品与完善服务保障
App automation testing Kaiyuan platform appium runner
