再次了解C
c的历史
- 1960 原型A语言->ALGOL语言
- 1963 CPL语言
- 1967 BCPL
- 1970 B语言
- 1973 C语言
C语言特点
- 基础性语言
- 语法简洁 紧凑 方便 灵活(得益于指针)
- 运算符 数据结构丰富
- 结构化 模块化编程
- 移植性好 执行效率高
- 允许直接对硬件操作
学习建议
- 概念的正确性
- 动手能力
- 主动阅读优秀的程序段
- 大量练习,编程是技术不是理论
学习思路
- 基本概念
- 数据类型 运算符 表达式
- 输入输出
- 流程控制
- 数组
- 指针
- 函数
- 构造类型
- 动态内存管理
- 常用库函数
- 调试工具和调试技巧
环境搭建与"Hello world"
环境
- 当前测试环境是安装了基于
Redhat
的Rocky
发行版,搭建在ESXI 6.4
虚拟机上
[root@node1 ~]# gcc --version
gcc (GCC) 11.3.1 20221121 (Red Hat 11.3.1-4)
Copyright © 2021 Free Software Foundation, Inc.
本程序是自由软件;请参看源代码的版权声明。本软件没有任何担保;
包括没有适销性和某一专用目的下的适用性担保。
# uname -a
Linux node1 5.14.0-284.11.1.el9_2.x86_64 #1 SMP PREEMPT_DYNAMIC Tue May 9 17:09:15 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux
"Hello world"
#inlcude <stdio.h>
#include <stdlib.h>
int main(void){
printf("hello world\n");
exit(0);
}
gcc 编译c的源文件过程:
gcc -v
线程模型:posix
Supported LTO compression algorithms: zlib zstd
gcc 版本 11.3.1 20221121 (Red Hat 11.3.1-4) (GCC)
C源文件->预处理->编译->汇编->链接->可执行文件
完整过程
- 预处理
gcc -E hello.c > hello.i
- 编译
gcc -S hello.i
- 汇编
gcc -c hello.s
- 链接->可执行文件
gcc hello.o -o hello
或者
gcc hello.c -o hello
又或者
make hello
执行
./hello
hello world
基本概念
怎么写代码
头文件的重要性
在c中,如果没有出现函数原型,就默认函数的返回值是int(老版本GCC)
#include <stdio.h>
int main()
{
int *num = malloc(sizeof(int));
*num = 100;
printf("%d\n",*num);
return 0;
}
hello.c: 在函数‘main’中:
hello.c:5:23: 警告:隐式声明函数‘malloc’ [-Wimplicit-function-declaration]
5 | int *num = (int *)malloc(sizeof(int));
| ^```~~
hello.c:5:23: 警告:隐式声明与内建函数‘malloc’不兼容
- 正确写法
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *num = (int *)malloc(sizeof(int));
return 0;
}
数据类型 运算符 表达式
- 基本类型
- 数值类型
- short
- int
- long
- float
- double
- 字符类型
- 数值类型
- 构造类型
- 数组
- 结构体 struct
- 共用体 union
- 枚举类型 enum
- 指针类型
- 空类型 void
254 -> unsigned int -> 32bit
(254)10 = (1111 1110)2 = (376)8 = (FE)16
254
B11111110(c不认识这个表示)
0376
0xFE
类型转换
隐式转化:
int i;
float f;
double d;
char ch;
//混合运算往精度高的类型靠
ch + i -> i
f - d -> d
(ch + i) - (float - double) -> double
bool类型
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h> //标准bool类型头文件
int main() {
bool a = false;
printf("a = %d\n", a);
exit(0);
}
浮点型的失真问题
int func(float f) {
if (f < 0) {
return -1;
} else if (fabs(f-0) <= 1e-6)
{ //极限方法 double fabs (double x) 返回浮点数 x 的绝对值
return 0;
} else {
return 1;
}
}
char 类型
在iso c中 char
有无符号是未定义行为
关于0的不同解释
0(整形) '0'(字符常量) "0"(字符串常量) '\0'(字 符常量)
类型匹配问题
数据类型要和后续代码中所使用的输入输出要相匹配(小心自相矛盾)
比如说对于定义了无符号整型,由于某些原因要赋值给整型,有可能会溢出int的范围
#include <stdlib.h>
#include <stdio.h>
int main() {
unsigned int a;
a = 1 << 31;
printf("%d", a);
}
正确
#include <stdlib.h>
#include <stdio.h>
int main() {
unsigned int a;
a = 1 << 31;
printf("%ud", a);
}
常量与变量
常量
- 整形常量(int): 1 890
- 实型常量(float): 1.2 3.14
- 字符常量(char):
'\t' '\n' '\0' '\015'(8进制) '\x7f' '\018'(错误的表示!!三个数是8进制标识不出现8)
- 字符串常量(构造类型来构造):
"" "a" "abXYZ" "abc\n\021\010"(a b c \n \021 \0 1 8)
- 标识常量:
#define
,在预处理阶段,不检查语法,只是简单的替换。
宏#define
的用法
直接替换宏体直接内容
这个部分在预处理的过程中不检测语法
#include <stdlib.h>
#include <stdio.h>
#define PI 3.1415926 //预处理解决掉了
#define ADD 2+3
// 正确写法
//#define ADD (2+3)
int main() {
int a,b,c;
a * PI; //在预处理会被直接替换为宏体内容
b + PI;
c / PI;
printf("%f\n", PI);
printf("%d\n", ADD * ADD);
}
:! gcc -E `PWD`/main.c
预编译结果:
# 3 "main.c" 2
# 8 "main.c"
int main() {
int a,b,c;
a * 3.1415926;
b + 3.1415926;
c / 3.1415926;
printf("%f\n", 3.1415926);
printf("%d\n", 2+3 * 2+3); //完整替换这里会出现优先级问题
exit(0);
}
建议在宏体内容括上括号
直接替换宏体函数
#include <stdlib.h>
#include <stdio.h>
#define MAX(a,b) ((a) > (b) ? (a) : (b))
int main() {
int a = 3, b = 5;
printf("%d\n",MAX(a, b));
}
/**
# 6 "main.c"
int main() {
int a = 3, b = 5;
printf("%d\n",(a > b ? a : b));
}
*/
如果把宏体加上括号就会出现以下情况:
[root@node1 ~]# cat main.c
#include <stdlib.h>
#include <stdio.h>
#define MAX(a,b) ((a) > (b) ? (a) : (b))
int main() {
int a = 3, b = 5;
printf("%d\n",MAX(a++, b++));
printf("%d\t %d\n",a,b);
}
[root@node1 ~]# ./main
6
4 7 #这里原本是5会自增两次
#预编译结果
# 6 "main.c"
int main() {
int a = 3, b = 5;
printf("%d\n",((a++) > (b++) ? (a++) : (b++))); #一目了然确定了问题所在
printf("%d\t %d\n",a,b);
}
在标准C对于以上无解,但我们可以用以下不是标准C方法技巧解决
#include <stdlib.h>
#include <stdio.h>
#define MAX(a,b) \
({int A=a,B=b; ((A) > (B) ? (A) : (B));})
//#define MAX(a,b) \
//({typeof(a) A=a,B=b; ((A) > (B) ? (A) : (B));}) typeof(a)获取a的类型来定义
//必须加括号
int main() {
int a = 3, b = 5;
printf("%d\n",MAX(a++, b++));
printf("%d\n",MAX(a++, b++));
}
#预编译结果
# 6 "main.c"
int main() {
int a = 3, b = 5;
printf("%d\n",({int A=a++,B=b++; ((A) > (B) ? (A) : (B));}));
printf("%d\n",({int A=a++,B=b++; ((A) > (B) ? (A) : (B));}));
}
在程序的预处理阶段,占编译时间,不占运行时间(没有函数调用的消耗),但不检查语法(比较危险)
变量
[存储类型] 数据类型 标识符 = 值
TYPE NAME = VALUE
- 标识符:由字母、数字、下划线组成且不能以数字开头的一个标识序列。如下文法:
letter -> a|b|...z|A|B|...|Z|_
digit -> 0|1|...|9
identifier -> letter(letter|digit)*
标识符: [_a-zA-Z][_a-zA-Z0-9]*
- 存储类型:
auto
:(默认) 自动分配空间( 没有指定存储类型时,缺省为auto,自动分配与回收),分配在栈空间上。得出的数值随机register
:(建议型)寄存器类型建议编译器分配在寄存器上
只能定义局部变量,不能定义全局变量,大小有限制,只能定义32位大小的数据类型,比如double
就不可以。因为寄存器没有地址,所以一个register类型的变量无法打印出地址查看或使用。inline
:从C++引入static
:(静态型) 一定自动初始化为0值或空值并且static变量的值有继承性。另外常用来修饰一个变量或者函数(防止当前函数对外扩展)extern
: (说明型) 意味着不能改变被说明的量的值或类型,可以用来扩展外部变量的作用域(不能写extern int a=1;来给外部整形变量a幅初值1编译会报错的)
- 数据类型 = 基本数据类型 + 构造类型
- 值:注意匹配数据类型即可
这里寄存器指的是x86/AMD的寄存器,把微机和单片机以及其他架构的处理器搞混了,学了微机原理你就知道了,PC机的CPU的寄存器是靠寄存器的名字寻址的,不是靠地址 寻址的,所以PC机的寄存器是没有地址的。
变量的生命周期与作用范围
static 与 auto
#include <stdlib.h>
#include <stdio.h>
void func() {
static int x = 1;
x++;
printf("%p->%d\n", &x ,x);
}
int main() {
func();
func();
func();
}
以上代码结果:
[root@node1 ~]# ./main
0x404024->2
0x404024->3
0x404024->4
#不使用static存储类型结果:
[root@node1 ~]# ./main
0x7ffffa47fc4c->2
0x7ffffa47fc4c->2
0x7ffffa47fc4c->2
全局变量与局部变量
全局变量的概念是从变量定义开始到代码结束 局变量的概念是从变量定义开始到代码块结束(语句体)
作用范围永远是内部屏蔽外部的
#include <stdio.h>
#include <stdlib.h>
int i=0;
void fun (void){
for(i= 0;i<5;i++)
printf("%c",'*');
printf("\n");
printf("[%s]%d\n", __FUNCTION__, i);
}
int main(){
for( i= 0;i<5;i++)
fun();
exit(0);
}
//运行结果:
cc fun.c -o fun
*****
[fun]5
补充project工程实现
多个c文件
使用 vim * -p
打开文件夹下所有文件,使用gt命令切换文件编辑。
#include <stdio.h>
#include <stdlib.h>
#include "proj.h"
int i=10;
int main(){
printf("[%s]:i = %d\n",__FUNCTION__,i);
fun();
exit(0);
}
//proj.c
#include <stdio.h>
#include <stdlib.h>
#include "proj.h"
int i =100;
void fun(void){
printf("[%s]:i = %d\n",__FUNCTION__,i);
exit(0);
}
#ifndef PROJ_H__
#define PROJ_H__
void fun(void);
#endif
结果会冲突:
[root@node1 minproject]# gcc main.c proj.c
/usr/bin/ld: /tmp/cc2v50cH.o:(.data+0x0): multiple definition of `i'; /tmp/cc63AEzU.o:(.data+0x0): first defined here
collect2: 错误:ld 返回 1
加了static后编译成功:
[root@node1 minproject]# gcc main.c proj.c
[root@node1 minproject]# ls
a.out main.c proj.c proj.h
[root@node1 minproject]# ./a.out
[main]:i = 10
[fun]:i = 100
以上例子说明常用来修饰一个变量或者函数(防止当前函数对外扩展)
同理在函数前面添加static:
static void fun(void){
printf("[%s]:i = %d\n",__FUNCTION__,i);
exit(0);
}
结果如下:
[root@node1 minproject]# gcc main.c proj.c
proj.c:5:1: 错误:对‘fun’的静态声明出现在非静态声明之后 #这里是语法错误
5 | static void fun(void){
| ^~~~~~
In file included from proj.c:3:
proj.h:3:6: 附注:previous declaration of ‘fun’ with type ‘void(void)’
3 | void fun(void);
| ^~~
[root@node1 minproject]# vim proj.h
[root@node1 minproject]# gcc main.c proj.c
In file included from main.c:4:
proj.h:3:13: 警告:‘fun’使用过但从未定义
3 | static void fun(void);#这里是运行错误
| ^~~
/usr/bin/ld: /tmp/ccVlu3p6.o: in function `main':
main.c:(.text+0x21): undefined reference to `fun'
collect2: 错误:ld 返回 1
以上例子也说明了static
常用来修饰一个变量或者函数(防止当前函数对外扩展),如何解决static隐藏函数?可以使用非静态函数call_fun来间接调用静态函数(就像面向对象的思路,c也可以间接完成面向对象的设计)。
extern的用法
#ifndef EXTERN_H__
#define EXTERN_H__
void func();
#endif
#include "extern.h"
extern int i; // 不定义 而是引用了其他地方的i ,也可以不用声明数据类型,编译器会帮你补充
int func() {
printf("[%s]%d\n", __FUNCTION__, i);
}
#include "stdlib.h"
#include "stdio.h"
#include "extern.h"
int i = 10;
int main() {
printf("[%s]%d\n", __FUNCTION__, i);
}
运算符与表达式
运算符
逻辑运算符的短路性
#include <stdio.h>
#include <stdlib.h>
int main() {
int a = 1, b = 2, c = 3, d = 4;
int m = 1, n = 1;
//逻辑与和或的短路性
(m = a > b) && (n = c > d);
//与运算如果左边的表达式为假没必要运算右边的表达式
//或运算如果左边的表达式为真没必要运算右边的表达式
printf("m = %d\n n = %d\n", m, n); // m : 0 n : 1
}
等号(赋值)扩展运算的顺序
从右往左计算数值
求字节数sizeof
#include <stdio.h>
#include <stdlib.h>
int main() {
printf("%lu, %lu, %lu, %lu, %lu, %lu, %lu\n",
sizeof(int),sizeof(short), sizeof(long),
sizeof(double), sizeof(float), sizeof(char), sizeof(void*));
}
4, 2, 8, 8, 4, 1, 8
位运算
- | 按位或
- & 按位与
- ^ 按位异或
- ~ 按位取反
应用
- 将操作数中的第n位置1 其他位不变
num = num | 1 << n;
- 将操作数中的第n位置0 其他位不变
num = num & ~(1<<n);
- 测试第n位:
if(num & (1<<n))
I/O操作
- 标准IO