编译
gcc main.c sub_func.c -o main -lm
-lm连接数学库;用到数学库的时候需要添加
一个工程,是编译所有c文件,利用空格分隔不同c文件
预处理 -E是参数 xxx.i 预处理文件
gcc -E xxx.c -o xxx.i
1) 头文件展开。---不检查语法错误,可以展开任意文件
2) 宏定义替换 ---将宏名替换成宏值
3) 注释替换 ---变成空行
4) 展开条件编译---根据条件来展开指令
#include <stdio.h>
#define PI 3.14
int main(void)
{
#ifdef PI
printf("1111111111111\n");
#endif
printf("hello world !\n");
return 0;
}
编译 -S是参数 生成汇编文件
1) 逐行检查语法错误 --整个编译4步骤中最耗时的
2) 将C语言翻译成汇编指令
汇编 gcc -c 1.s -o 1.o -c就是参数
生成目标文件(二进制机器指令,实际显示时候十六进制形式)
1) 将汇编指令翻译成对应的二进制编码
链接 gcc 1.o -o 1.exe 无参数
生成可执行文件
1) 数据段合并
2) 数据地址回填
3) 库引用

gcc 1.c -o 1.exe
这一步其实就相当于上面四步的合集
类型限定符:
extern 声明一个变量,extern声明的变量没有建立存储空间,
extern int a;//变量在定义的时候创建存储空间
const 定义一个常量,常量的值不能修改
volatile 防止编译器优化代码;
register 定义寄存器变量(无地址,无法用指针操作),提高效率;register是建议型的指令,而不是命令型的指令,
如果CPU有空闲寄存器,那么register就生效,如果没有空闲寄存器,那么register无效
类型指定符、基本类型、标识符、关键字
- 这条指令所声明的存储单元有多大呢?
- 这个存储单元用于存储哪种类型的数据呢?(或者说应该按照哪种规则来访问这个存储单元呢?)
int:类型指定符
int A;==signed int A;==signed A;->用于存储"有符号整数
int: C语言中的基本类型之一
C语言规定:int类型的存储空间,"最小"是两个字节,它的大小会根据不同的编译器而有所不同
- 该如何访问它呢?
A: 标识符
标识符命名规则:
1. 可以由下划线,26个大写英文字母,26个小写英文字母,0-9的数字所组成
2. 不能以数字开头
3. 不能和关键字重名
4. 标识符长度建议控制在31字符以内(因为基本上所有编译器最低都支持这个长度)
int: 关键字
1. C语言专属标识符
2. 编译器赋予了它固定的意义和用途
3. 错误的例子: int int;
C语言区分大小写:
1. A和a是完全不同的2个标识符
2. 错误的例子: iNT A;
基本类型之标准整数类型


Bool类型存储0则解析为0,非0则统一解析为1
类型的多种写法之类型指定符
11个类型指定符号:char、short、int、long、signed、unsigned、_Bool、void、float、double、_Complex
- 类型只有一种写法、不存在多种写法
每个类型本身只有一个名字
- 用于声明变量的不是类型,而是类型所对应的"类型指定符"
- 一个类型可能对应着多个"类型指定符"
long int类型对应4中不同的"类型指定符",分别是:long int、long、signed long、signed long int
short int-> short、signed short、sigend short int
int-> signed、signed int
long int->long、signed long、signed long int
long long int->long long、signed long long、signed long long int
unsigned short int-> unsigned short
unsigned int -> unsigned
unsigned long int->unsigned long
unsigned long long int->unsigned long long
表达式
表达式 n < =100
"<=" ->关系运算符->关系表达式
3个表达式的值:(每个表达式都有一个值)
表达式 n<=100: 运算符的运算结果
表达式n:变量n中所存储的值
表达式100(常量表达式):100
"<="运算符的运算结果怎么判定呢?
C语言规定:满足条件时:运算结果=1
不满足条件时:运算结果=1
while循环:表达式的值=0,结束循环
表达式的值!=0,继续循环
表达式最简单的形式:单独的一个常量或者变量
结合性
sum=n=0;
运算符的结合性:
左边的优先级高于右边的,我们称之为"从左向右结合"
右边的优先级高于左边的,我们称之为"从右向左结合"
C语言规定:赋值运算符是"从右向左结合"
右边"="的操作数:它左边的m,它右边的0
左边"="的操作数:它左边的sum,它右边的n=0
计算表达式的值,简称"值计算"
我们的主要目的是为了进行"值计算"
改变变量的值只是附带的一个作用,我们称之为"副作用"
逻辑运算符
- && 与运算-》只要有一个操作数的值是0,结果就是0
- || 或运算-》只要有一个操作数的值是1,结果就是1
unsigned long long int cusum(unsigned long long);//声明函数原型
//unsigned long long int cusum(unsigned long long r);//声明函数原型,参数标识符可以不要,因为只是编译器语法检查而已
int main(){
/* unsigned long long int x,y,z;
x=cusum(100);
y=cusum(200);
z=cusum(300);
*/
//初始化器
unsigned long long int x=cusum(100),y=cusum(200),z=cusum(300);
return 0;
}
unsigned long long int cusum(unsigned long long r){//声明函数、函数定义
//unsigned long long int n,sum;
// n=1;
// sum=0;
unsigned long long int n=1,sum=0;
//while(n<=r){
//sum+=n++;
// sum=sum+n++;
// sum+=n;//复合赋值运算符,1条机器指令,性能更高
//n++;
//sum=sum+n;//简单赋值,2条机器指令
//n=n+1;
//}
//C语言规定while没有{}则只执行紧跟着的一句代码
//while(n<=r)sum+=n++;
/*
,--》逗号运算符-》逗号表达式
执行流程--》先处理左操作数(sum+=n++),再处理右操作数
逗号表达式的值(sum+=n++,n<=r)--》右操作数的值
*/
//务必加上;代表空语句,如果没有;则while会把下一条语句当成循环体,此时如果没有;则会执行 return sum;
while(sum+=n++,n<=r);
//该句如果没有的话,会返回随机值
return sum;
}
关键字
数据类型相关的关键字
char short int long float double
struct union enum signed unsigned void
- char 字符型,占一个字节
char ch='1';
char ch=1;
//两者不同,字符'1'其实相当于是ascil码赋值
- short 短整型 占2个字节
- int 整型,在32位系统下占4个字节,16位平台下占2个字节
- long 长整型 32位系统下4个字节 (windows系统下不论32还是64都是4字节,linux系统下是32位4字节,64位8字节)
- float 单浮点型,4个字节
- double 双浮点型,8个字节
- struct 定义结构体
- union 联合体关键字
- signed 有符号
在定义char 整型(short int long)数据的时候用signed修改,代表可存储正数和负数
- unsigned 无符号
在定义char 整型(short int long)数据的时候用unsigned修改,代表可存储正数和0
实型
- float: 单精度浮点型 4字节 %f
- double:双精度浮点型 8字节 %lf
- 浮点数默认是double
#include <stdio.h>
int main(void)
{
float x=3.14; //默认是double,
//之所以此时也可以是因为double自动类型转换为float
// float x1=3.14f;
return 0;
}
sizeof关键字:
- sizeof不是函数,所以不需要包含头文件,它的功能是计算一个数据类型的大小,单位为字节。
- sizeof的返回值为size_t
- size_t类型在32位操作系统下是unsigned int,是一个无符号的整数
- 但是经过测试在wsl64位ubuntu系统容下是需要lu或者ld形式才能正确打印的
- 如下是定义size_t转到的定义:可知在ubuntu64位下面是 long unsigned int
#define __SIZE_TYPE__ long unsigned int
#endif
#if !(defined (__GNUG__) && defined (size_t))
typedef __SIZE_TYPE__ size_t;
字符小案例
#include <stdio.h>
int main(void)
{
//A 65 a 97
char ch='M';
printf("%c\n",ch+32);//m
return 0;
}
#include<stdio.h>
int main()
{
char ch='a';
printf("ch=%c\n",ch); //ch=a
printf("ch=%d\n",ch); //ch=97 ascil码
return 0;
}
特殊意义转义字符表

计算机中都是补码形式存储数据的
正数:原码补码反码相同
负数:首位为符号位1
反码:符号位不变,其他取反
补码:反码+1
数据在内存里是以补bai码的形式存储的原因有三点:
1、保证了0的唯一性,保证了数的表示的准确性。
2、让加减可以统一处理,优化了数的运算过程。
3、解决了自身逻辑意义的完整性。
#include<stdio.h>
int main()
{
int a=-5;
printf("a=%d\n",a); //a=-5
//x代表十六进制,但是十六进制只有正数,所以显示其实是补码的十六进制形式
printf("a=%x\n",a); //ch=fffffffb
return 0;
}
- 测试在64位系统下各种数据类型在内存中占的字节数
#include<stdio.h>
int main()
{
char a='a';
short int b=10;
int c;
long int d;
float e;
double f;
printf("%d\n",sizeof(a)); //1
printf("%d\n",sizeof(b)); //2
printf("%d\n",sizeof(c)); //4
printf("%d\n",sizeof(d)); //4
printf("%d\n",sizeof(e)); //4
printf("%d\n",sizeof(f)); //8
//以下结果同上
printf("%d\n",sizeof(char));
printf("%d\n",sizeof(short int));
printf("%d\n",sizeof(int));
printf("%d\n",sizeof(long int));
printf("%d\n",sizeof(float));
printf("%d\n",sizeof(double));
return 0;
}
输出类型
int %d
short %hd
long %ld
long long %lld
无符号
unsigned int %u
unsigned short %hu
unsigned long %lu
unsigned long long %llu
void
void不能定义变量,void是用于修饰函数的参数或者返回值,代表函数无参或者没有返回值
存储相关关键字
register
是寄存器的意思,用register修饰的变量是寄存器变量;即在编译的时候告诉寄存器这个变量是寄存器变量,
尽量
将其存储空间分配在寄存器中,如果寄存器满了,那也只能放到内存中
- 定义的变量不一定真的存放在寄存器中
- cpu取数据的时候去寄存器中拿数据比去内存中拿数据要快
- 因为寄存器比较宝贵,所以不能定义寄存器数组
- register只能修饰
字符型
以及整型
,不能修饰浮点型
register char ch;
register short int b;
register int c;
register float f;//错误的
- 因为register修饰的变量可能存放在寄存器中不存放在内存中,所以不能对寄存器变量取地址。因为只有存放在内存中的数据才有地址
static
static可以修饰全局变量,局部变量,函数
常量:不会变化的数据
1. "hello" 'A' -10 3.14(浮点常量)
2. #define PI 3.1415 【推荐】【强调:后面没有分号结束标记】
3. const int a=10;
const关键字,表示只读,但是通过指针可以修改
const
const是常量的意思,用const修饰的变量是只读的,不能修改它的值
const int a=100;//在定义时候用const修饰,并赋初值
a=111;//再修改就是错误的
//const可以修饰指针
- 全局常量,在常量区;即使通过指针修改const修饰常量也会报错
const int a=10;//常量区
int main()
{
int *p=&a;
*p=100;
return 0;
}
- 局部常量,存放在栈上,有警告,但是能通过指针修改值
int main()
{
const int a = 10;//存放在栈上,是伪常量
int *p = &a;
*p = 100; //报警告,但是还是能运行的
printf("%d\n",a);//100
return 0;
}
- 指针常量和常量指针
指针和 const 谁在前先读谁 ;
谁在前面谁就不允许改变。
例如:
int const *p1 = &b; //const 在前,定义为常量指针
int *const p2 = &c; // *在前,定义为指针常量
常量指针是指指向常量的指针,顾名思义,就是指针指向的是常量,即,它不能指向变量,
它指向的内容不能被改变,不能通过指针来修改它指向的内容,但是指针自身不是常量,
它自身的值可以改变,从而指向另一个常量。
指针常量是指指针本身是常量。它指向的地址是不可改变的,但地址里的内容可以通过指针改变。
它指向的地址将伴其一生,直到生命周期结束。有一点需要注意的是,指针常量在定义时必须同时赋初值。
//理论上参数一也可以是 int *p;但是*p也可以是单个整数的指针,所以为了更具象,推荐下面的定义方式
void printArr(int arr[],int size){
}
int main()
{
//一维数组是不是指针?
int arr[5]={1,2,3,4,5};
printf("%ld\n",sizeof(arr));//20
//有两种特殊情况,一维数组不是指向第一个元素的指针
//1. sizeof
//2. 对数组名取地址,得到数组指针,步长是整个数组的长度
printf("%p\n",&arr);//0x7fffda596530
printf("%p\n",&arr+1);//0x7fffda596544
//arr数组名 它是一个指针常量,指针的指向不可以修改的,而指针指向的值可以修改 int *const a;
//数组索引,可以是负数
int *p=arr;
p=p+3;
p[-1];//实际上打印就是3,本质相当于*(p-1)
return(0);
}
- 字符串常量
int main()
{
char *c1="hello";
char *c2="hello";
char *c3="hello";
//全部地址相同,证明用的同一份
printf("%p\n", c1); //0x7f38bf200764
printf("%p\n", c2); //0x7f38bf200764
printf("%p\n", c3); //0x7f38bf200764
printf("%p\n", &"hello"); //0x7f38bf200764
//ANSI标准中没有规定,字符串常量内部字符能否修改,不同编译器实现不同,所以建议不修改(常规)
// c1[0]='d';
return 0;
}
auto
auto int a和int a是等价的,auto关键字现在基本不用了
extern
是外部的意思,一般用于函数和全局变量的声明
- 变量定义会开辟内存空间,变量声明不会开辟内存空间
- 当编译器编译程序时,在变量使用之前,如果没有看到变量定义;编译器会自动寻找一个变量声明提升为定义。
- 如果该声明前有extern关键字,则无法提升
int main()
{
//告诉编译器,下面代码出现a不要报错,是外部链接属性;在其他文件中
//如果没有这句则下面a使用,编译都通过不了
extern int a;
//如果加了上面这句,会在整个工程中查找a,如果能找到则使用;该句不开辟内存,只是声明作用
printf("%d\n",a);
//假如在另一个文件中有int a=100;实际上在C语言中,全局变量
//前都隐式加了extern关键字;然后当前文件才能使用
return 0;
}
控制语句相关的关键字
if else break continue for
while do switch case
goto default
其他关键字
sizeof : 用来测变量、数组的占用内存空间的大小(字节数)
typedef : 重命名相关的关键字,给一个已有的类型,重新起一个类型名,并没有创建一个新的类型
volatile : 定义的变量是易改变的,即告诉cpu每次用volatile变量的时候,重新去内存中取保证用的是最新的值,而不是寄存器的备份
#include<stdio.h>
//short int b;
//short int INT16;
//针对这种基本数据类型可以省略上面两步,直接定义,但是其他复杂的则不能省略
typedef short int INT16;
int main()
{
short int a=101;
INT16 b=111;
printf("%d\n",a);
printf("%d\n",b);
return 0;
}
C 库函数 - memset()
C 库函数 void *memset(void *str, int c, size_t n) 复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符。常用来置空
void *memset(void *str, int c, size_t n)
- str -- 指向要填充的内存块。
- c -- 要被设置的值。该值以 int 形式传递,但是函数在填充内存块时是使用该值的无符号字符形式。
- n -- 要被设置为该值的字符数。
#include <stdio.h>
#include <string.h>
int main ()
{
char str[50];
strcpy(str,"This is string.h library function");
puts(str);
memset(str,'$',7);
puts(str);
return(0);
}
This is string.h library function
$$$$$$$ string.h library function
char *temp=malloc(100);
memset(temp,0,100);
数据类型
基本类型
char
short int
int
long int
float
double
构造类型
由若干个相同或不同类型数据构成的集合,这种数据类型被称为构造类型
例如:数组、结构体、共用体、枚举
字符串常量与字符常量的不同
'a'为字符常量,"a"是字符串常量
每个字符串的结尾,编译器会自动的添加一个结束标志位'\0',即"a"包含两个字符'a'和'\0'
格式化输出字符
%d 十进制有符号整数
%u 十进制无符号整数
%x 十六进制整数
%o 八进制整数
%f float型浮点数
%lf double型浮点数
%e 指数形式的浮点数
%s 字符串
%c 单个字符
%p 指针的值
- 特殊应用
%3d: 要求宽度为3位,不足3位前面空格补齐,如果足够3位,此语句无效
%03d: 要求宽度为3位,不足3位前面0补齐,如果足够3位,此语句无效
%-3d: 要求宽度为3位,如果不足3位,后面空格补齐,如果足够3位,此语句无效
%.2f: 小数点后只保留2位
%5.2f: 小数点后只保留2位,且加上.需要五位,前面空格补齐
//由此可知还可以互相组合使用
例如:%05.2f :小数点只保留2位,并且总共五位,不足前面补0
类型转换
自动类型转换
- 占用内存字节数少(值域小)的类型,向占用内存字节数多(值域大)的类型转换,以保证精度不降低
-
转换方向
1.jpg
- 当表达式出现了char、short int、int类型中的一种或者多种,没有其他类型,则参加运算的成员全部变成int类型的参加运算,结果也是int类型
- 当表达式出现了带小数点的实数,参加运算的成员变量全部变成double类型的参加运算,结果也是double类型
- 当表达式有有符号数,也有无符号数,参加运算的成员变成无符号数参加运算结果也是无符号数(前提是表达式中无实数)
- 在赋值语句中等号右边的类型自动转换为等号左边的类型
- 自动类型转换(以及强制类型转换)都是在运算的过程中进行临时性的转换,并不会影响自动类型转换的变量的值和其类型
#include<stdio.h>
int main()
{
int a=-8;
unsigned int b=7;
float c=5.8f;//5.8后面加f代表是float类型,不加的话默认是double类型
int d;
printf("%d\n",5/2);//2
printf("%lf\n",5.0/2);//2.500000
printf("%lf\n",-5.0/b);//-0.714286
printf("%d\n",-5/b);//613566755
d=c;
printf("d=%d\n",d);//d=5
if (a+b>0)
{
printf("a+b>0\n");//一直输出都是这个
}else
{
printf("a+b<0\n");
}
printf("%x\n",(a+b));//ffffffff,十六进制没有负数一说,所以都是当作无符号处理
printf("%d\n",(a+b));//-1,因为本质上计算是一致的只是此处%d会转换成有符号输出,所以-1
return 0;
}
强制类型转换
(类型说明符)(表达式)
例子:
(float)a;
(int)(x+y);
运算符
- &:按位与,任何值与0得0,与1保持不变
- |:按位或,任何值或1得1,与0保持不变
- ~:按位取反
-
>>
:右移分为:逻辑右移,算数右移;具体是哪种右移,需要区分编译器- 逻辑右移:高位补0,低位溢出;
- 算数右移(针对负数情况,结果依赖于机器,空出的位可能用0填充,也可能用符号位填充)
- 有符号数
- 正数:高位补0,低位溢出
- 负数:高位补符号位,低位溢出
- 无符号数:高位补0,低位溢出
- 有符号数
-
<<
:高位溢出,低位补0
#include<stdio.h>
int main()
{
//计算机存储的都是补码形式,所以-1的补码是ffffffff,实际上移位运算也是补码移位
//根据输出结果可知,只有算术移位才能使补充符号位,然后移位完毕之后还是ffffffff的补码,补码一致才说明逆推还是-1
printf("%d\n",-1>>3);//-1 ubuntu证明gcc编译器是算数移位
return 0;
}
- ,逗号运算符的结果是,后面表达式的结果;且逗号运算符的优先级最低
//(A,B,C):表达式从左向右依次执行,最后一个表达式的值就是逗号运算符表达式的值;且逗号表达式必须加()
#include<stdio.h>
int main()
{
int num;
num=(5,6);
printf("%d\n",num);//6
return 0;
}
int main(void)
{
int a=1,b=2,c=3;
int d=(a=4,b=5,c=6);
printf("%d %d %d\n",a,b,c);//4 5 6
printf("%d\n",d);//6
return 0;
}
#include<stdio.h>
int main()
{
int num;
//=运算符优先级高于逗号运算符
num=5,6;
printf("%d\n",num);//5
return 0;
}
#include<stdio.h>
int main()
{
int num;
num=(5,6,7);
printf("%d\n",num);//7
return 0;
}
-
switch:switch(表达式)只能使字符型或者整型的(short int int long int),
不能是浮点数
-
for循环
#include<stdio.h>
int main()
{
//c99:如果想int i直接放在for()内部需要C99才支持
int i;
for (i = 0; i < 100; i++)
{
printf("%d\n",i);
}
return 0;
}
注意:结合逗号表达式使用for循环
#include <stdio.h>
int main()
{
int i=0;
int a=0;
for(i=1,a=3 ;i<10,a<20;i++,a+=5){
printf("i=%d\n",i);
printf("a=%d\n",a);
}
return 0;
}
/**
i=1
a=3
i=2
a=8
i=3
a=13
i=4
a=18
*/
逗号表达式虽然是逗号之后的作为返回值,但是在for循环中,实际上还是每个都执行了,而且,i<10,a<20实际上相当于&&,所以才会只输出这么几条数据
- goto
一般不建议使用,调试代码时候用用就行,太灵活了;而且goto跳转到的标签必须是
函数
(只要一个函数内部即使两个for循环中都可以)内部,否则无法生效
#include<stdio.h>
int main()
{
printf("1\n");
printf("2\n");
goto zq;
printf("3\n");
printf("4\n");
printf("5\n");
zq:
printf("6\n");
//输出 1,2,6
return 0;
}
数组
- 数组的定义
- 数组名其实就是当前数组的首地址(重要)
#include <stdio.h>
int main()
{
int a[5];
//定义一维数组可以不给个数
int b[] = {1, 2, 3, 4};
//定义二维素组给以不给行数,必须给列数
int a1[][3] = {{1, 2}, {4, 6}};
int b1[2][4];
//指针数组
char *a[10]
//结构体数组
struct stu boy[10]
//浮点型数组
float a[6];
a[4]=3.14f;
return 0;
}
- 数组初始化
#include <stdio.h>
int main()
{
//全部初始化
int a[5] = {1, 2, 3, 4};
//部分初始化,初始化赋值不够则后面的补0
int b[3] = {1, 2};
//默认值是随机数的情况
int arr[10];//打印发现全是随机数
int i;
//遍历数组
for (i = 0; i < sizeof(b)/sizeof(int); i++)
{
printf("b[%d]=%d\n",i, b[i]); //1 2 0
}
//二维数组初始化
//1。按行初始化
int c[2][2]={{1,2},{4,5}};//全部初始化
int c1[2][2]={{1,2},{1}};//部分初始化
//2.逐个初始化:此种方式关注下,因为二维数组虽然逻辑上有行列,但是内存中实际是挨个存储的
int d[2][3]={1,2,3,4,5,6};//全部初始化
int d1[2][3]={1,2,3,4};//部分初始化
//打印二维数组,一般是直接写死len,但是也可以如下写活
int arr[2][3] = {1, 2, 3, 4, 5};
// printf("%ld\n",sizeof(arr) / sizeof(arr[0]));
// printf("%ld\n",sizeof(arr[0]) / sizeof(arr[0][0]));
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
for (int j = 0; j < sizeof(arr[0]) / sizeof(int); j++)
{
printf("%d\n", arr[i][j]);
}
}
return 0;
}
- 二维数组地址合一
#include <stdio.h>
int main()
{
int arr[2][3] = {1, 2, 3, 4, 5};
printf("%p\n",arr);
printf("%p\n",&arr[0][0]);
printf("%p\n",&arr[0]);
printf("%p\n",arr[0]);
return 0;
}
上面四个完全相同
- 字符数组的定义
-
用字符串方式赋值比用字符逐个赋值要多占1个字节'\0';
-
上面的数组c2在内存中的实际存放情况为:
2.jpg
-
由于采用了'\0'标值,字符数组的输入输出将变得简单,直接%s利用printf输出即可,因为系统遇到'\0'自动停止
-
#include <stdio.h>
int main()
{
char c1[]={'c',' ','p','o'};
char c2[]="c prog";
char a[][5]={
{'b','a','c'},
{'b','a','c','e','x'}
};
char a1[][6]={"hello","world"};
char str[15];
printf("input string:\n);
scanf("%s",str);//此种方式获取输入字符串,字符串不能有空格,空格会被当作结束标值,后面的输入全都会被舍弃
printf("output:%s\n",str);
//字符数组
char str[3]={'h','e','l'};
//字符串
char str[4]={'h','e','l','\0'};
//字符串
char str[]="hel";
return 0;
}
也就是说:字符数组包含字符串
- putchar:输出一个字符到屏幕
int main(void)
{
putchar(97);//a
putchar('b');//b
return 0;
}
scanf函数:从键盘接受用户输入
接收字符串的时候:
- 具有安全隐患,如果存储空间不足,数据能够存储到内存中,但不被保护(在linux平台会报错,在vs中可能能正常接收)
- scanf函数接收字符串时,碰到空格和换行会自动终止。不能使用scanf接收带有空格的字符串
#include <stdio.h>
int main(void)
{
// int a;
// scanf("%d",&a);
// printf("%d\n",a);
//接收多个数字
// int b,c,d;
// scanf("%d %d %d",&b,&c,&d);
//如上接收多个数字的时候因为不知道怎么区分,
//所以最好是接收时候%d之间就有空格,这样就能正确识别了
//接收超过存储空间的字符串
char s[5];
//注意:数组的变量名就是地址,所以不需要&
scanf("%s",s);
printf("%s",s);
return 0;
}
- getchar()函数:从键盘获取输入的一个字符,返回该字符的ASCII码
int main(void)
{
char ch;
ch = getchar();
printf("%c", ch);
return 0;
}
#include <stdio.h>
int main(void)
{
int a=1,b=2,c=3;
int d=(a=4,b=5,c=6);
printf("%d %d %d\n",a,b,c);//4 5 6
printf("%d\n",d);//6
return 0;
}
- 字符串操作函数
#include <stdio.h>
#include <string.h>
int main()
{
//字符串获取
//注意:1. 用于存储字符串的空间必须足够大,防止溢出。char str[5]
// 2. 获取字符串,%s 遇到空格和\n终止;但是借助正则表达式,可以实现scanf("%[^\n]s",str)//除去\n都接收包括空格
//例如:scanf接收hello,实际上应该给空间大小是6,否则最后\0无法接收;虽然直接打印printf是正常输出,但是中间有特殊操作时候,\0可能被覆盖
//字符串操作函数
// gets: char *gets(char *s) 但是该函数在c11已经被移除
// 从标准输入读入字符,并保存到s指定的内存空间,直到出现换行符或读到文件结尾为止(允许空格),因此容易导致字符数组越界(缓冲区溢出)的情况
// 获取一个字符串,返回字符串的首地址
// fgets: char* fgets(char*s,int size,FILE *stream);
// 从stream指定的文件内读入字符,保存到s所指定的内存空间,直到出现换行符、读到文件结尾或已读了size-1个字符为止,最后会自动加上\0作为字符串结束
// 空间足够读\n(换行),空间不足舍弃\n
// 参数一:字符串
// 参数二:指定最大读取字符串的长度(size-1)
// 参数三:stream:文件指针,如果读键盘输入的字符串,固定写为stdin
// char str[100];
//printf("%s\n", fgets(str,sizeof(str),stdin));
// puts:int puts(const char*s)
// 将一个字符串写出到屏幕;返回值:成功返回非负数,失败返回-1;输出字符串之后会自动添加\n换行符
// char str[]="hello world";
// puts(str);
// fputs: int fputs(const char* str,FILE * stream)
// FILE:文件指针,如果屏幕标准输出,则固定写为stdout
// 输出字符串之后不添加\n换行符
// strlen:计算指定字符串的长度,不包含字符串结束符\0
// 需要导入#include <string.h>
char str[]="hello world";
puts(str);
printf("%ld\n",strlen(str));//11
return 0;
}
sizeof本质是操作符,不是函数
三种使用方式
- sizeof(类型)
- sizeof(变量)
- sizeof 变量
函数
注意函数隐式声明,如果想不起来,记得复习
默认编译器做隐式声明函数时,返回都是int,根据调用语句补全函数名和形参列表
#include <stdio.h>
//也可以通过间接函数声明,然后引入头文件即可。利用extern关键字
//实际上include内部也是extern;只不过可以把extern 和 include的区别当做是“零售”与“批发”的区别。include是批发,而extern 则是零售。
//extern:在每个用到的文件中需要用多个extern 声明;
//include:只需要在include各声明一次,其它使用这些变量的只需要包含该头文件即可.
//直接函数声明:当函数实体在主调函数下面时候,需要声明,因为编译器是从上往下进行的找不到声明会报错
int max(int x, int y);
int main()
{
int a = max(1, 2);
printf("%d\n", a);
return 0;
}
int max(int x, int y)
{
int num;
if (x > y)
{
num = x;
}
else
{
num = y;
}
return num;
}
函数头文件
//main.c
#include <stdio.h>
//自定义的头文件,要用双引号
#include "myfun.h"
int main(int argc, char *argv[])
{
myfun();
return 0;
}
//myfun.c
void myfun(){
printf("hello");
}
//myfun.h
#ifndef MYFUN_H
#define MYFUN_H
void myfun();
#endif // MYFUN_H
防止头文件重复包含:
1) #pragma once ->windows中使用
2) #ifndef _HEAD_H_
#define _HEAD_H_
include头文件
函数声明
类型定义
宏定义
#endif
- 函数调用惯例
调用惯例 | 出栈方 | 参数传递 | 名字修饰 |
---|---|---|---|
cdecl | 函数调用方 | 从右至左的顺序压参数入栈 | 下划线+函数名 |
stdcall | 函数本身 | 从右至左的顺序压参数入栈 | 下划线+函数名+@+参数字节数,如函数int func(int a, double b)的修饰名是_func@12 |
fastcall | 函数本身 | 头两个DWORD(4字节)类型或者占用更少字节的参数被放入寄存器,其他剩下的参数按从右到左的顺序压入栈 | @+函数名+@+参数的字节数 |
pascal | 函数本身 | 从左至右的顺序压参数入栈 | 较为复杂,参加pascal文档 |
此外,不少编译器还提供一种称为naked call的调用惯例,这种调用惯例用在特殊的场合,其特点是编译器不产生任何保护寄存器的代码,故称naked call。
对c++而言,以上几种调用惯例的名字修饰策略都略有所改变,因为c++支持函数重载以及命名空间和成员函数等等,因此实际上一个 函数名可以对应多个函数定义,那么上面提到的名字修饰策略显然无法区分各个不同同名函数定义的。所以c++有自己更加复杂的名字修饰策略。最后,c++还有自己一种特殊的调用惯例,称为thiscall,专门用于类成员函数的调用,其特点随编译器不同而不同,在vc里是将this指针存放于eax寄存器,参数从右到左压栈,而对于gcc、thiscall和cdecl完全一样,只是将this看作是函数的第一参数。
总结:C语言中,就是cdel;所谓出栈其实就是回收
存储器
- 外存
外存又叫外部存储器,长期存放数据,掉电不丢失数据,常见的有:硬盘,flash,rom,光盘等
- 内存
内存又叫内部存储器,暂时存放数据,掉电数据丢失,常见的内存设备:ram,DDR(内存条)
内存的区分
- 物理内存:实实在在存在的存储设备
- 虚拟内存:操作系统虚拟出来的内存
- 操作系统会在物理内存和虚拟内存之间做映射
- 在32位系统下,每个进程的寻址范围是4G,0x00000000 ~0xffffffff,程序里面看到的都是虚拟内存
- 在32位系统中,虚拟内存被分为两个部分,3G的用户空间和1G内核空间,其中用户空间是当前进程私有的,内核空间是一个系统中所有进程公有的
虚拟内存分区
- 堆: 在动态申请内存的时候,在堆里开辟内存
- 栈: 主要存放局部变量
- 静态全局区:
-
未初始化的静态全局区
静态变量(定义变量的时候,前面加static修饰),或全局变量,没有初始化的,存在此区
-
初始化的静态全局区
全局变量,静态变量,赋过初值的,存放在此区
-
- 代码区/段:存程序代码(二进制形式)
- 文字常量区:存放常量
内存4区模型
上面虚拟内存分区是一种方式,下面也是一种方式
- 代码段:.text段,程序源代码(二进制形式)
- 数据段: 只读数据段 .rodata段;初始化数据段 .data段;未初始化数据段 .bss段
- stack:栈,在其上开辟栈帧,windows默认1M可以设置为最大10M;Linux默认8M可以设置为最大16M
- heap:堆;给用户自定义数据提供空间。约1.3G+

Linux系统图中,只是拿32位系统(4G)做示例,实际上扩大也相同;堆的内存地址是从打到大;栈内存地址分配是从大到小;可以从图上箭头顺序得出结论;栈底高地址,栈顶低地址,也符合如图从大到小,栈顶数据后放入的
- 内存存放方向
上面可知栈的地址生长方式,但是具体到栈内部某条数据的字节存储方向如下:小端对齐
int main()
{
//从前往后是高位字节到低位字节
int a=0x11223344;
char *p=&a;
printf("%x\n",*p);//44 低位字节数据 低地址
printf("%x\n",*(p+1));//33 高位字节数据 高地址
// 小端对齐:高对高,低对低
return 0;
}
普通全局变量
int num=100;//全局变量
int main(){return 0;}
作用范围程序的所有地方,只不过使用之前需要声明,如果想在其他c文件中使用,需要extern int num;注意声明的时候,不需要赋值;一直存在直到程序结束;不赋初值的话,默认是0
静态变量static
//在运行前分配内存,程序运行结束,生命周期结束
static int num=100;//静态全局变量
int main(){
return 0;
}
static限定了静态全局变量的作用范围,只能在定义它的.c中使用,;如果不赋初值默认0
静态局部变量
- 在它定义的函数或复合语句中有效
- 第一次调用的时候,开辟空间赋值,函数结束后不释放,以后再调用函数的时候,不再为其开辟空间,也不赋初值,用的还是之前的变量
- 不赋初值则默认值是0
void test(){
static int num=1;
num++;
printf("%d\n",num);
}
int main(int argc, char *argv[])
{
test();//2
test();//3
return 0;
}
普通局部变量
- 在函数内部定义的,或者复合语句中定义的变量
int main{
//不初始化的话,默认值不同平台不同,可以简单理解为默认值不确定
int num;//局部变量
{
int a;//局部变量
}
}
地址运算符和间接运算符
- & --->地址运算符--->地址表达式--->表达式的值?--->变量的内存地址
-
*
--->间接运算符--->间接表达式--->表达式的值?--->地址所指向的值
共同特点:
- 一元运算符,只需要一个右操作数
- 优先级相同,从右往左结合
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a=100;
int * b;//存储指针
b=&a;//取地址 指向变量a的指针-->变量a是int类型的 -->指向int类型的指针
*&a;//取地址对应的值,此时是100
printf("Hello world!\n");
return 0;
}
#include <stdio.h>
void swap(int *a,int *b){
int temp=*b;
*b=*a;
*a=temp;
}
int main()
{
int m=100,n=1000;
/*
函数名->隐式的转换->函数的地址->等价于:(&swap)(&m,&n);
函数名->隐式的转换->"指向函数"的指针(地址)
*/
swap(&m,&n);
printf("%d,%d\n",m,n);//1000,100
//十分重要,类型必须严格匹配才能这样赋值
void (*p)(int *,int *)=swap;
p(&m,&n);
printf("%d,%d",m,n);//100,1000
//综上所述:这几种也等效
//(*p)(&m,&n); (&*p)(&m,&n);(*&*p)(&m,&n);(&*&*p)(&m,&n)
return 0;
}
全局函数
就是正常书写的函数
static函数(静态函数/内部函数)
- 在定义函数的时候,返回值类型前面加static修饰,这样的函数称为内部函数。
- static限定了函数的作用范围,在定义的.c中有效
static void test(){
//static函数只能够在当前.c文件中使用。其他文件即使声明也无效
}
常量类型
- 常量也是有类型的,怎么判断呢?
- 整数类型的常量-》简称整型常量
- 整型常量3种进制的写法(前缀)
a=125;//十进制写法,以非0的数字开头
a=0125;//八进制写法,以0开头
a=0x125;//十六进制写法,以0x/0X开头
a=0b00001;//二进制写法,以0b/0B开头
//注意:c语言并没有规定二进制的写法啊,这是mingw编译器自行添加的,所以换一个编译器,可能程序报错
- 整型常量的3个后缀:u、l、ll、(不分区大小写)
a=125u;
a=125L;
a=125LL;
a=125ul;//u和l,谁前后都一样
a=125ull;//u和\l,谁前后都一样
前缀+后缀+具体C实现,决定了一个常量的类型;可以通过debuger的watch查看,或者整型常量类型对照表

由上图可知:即使无后置也无前缀(十进制),则进入第一列的第一行的三种,存储的数据,int放不下,会自动类型变为long int,同理还有long long int;可通过debuger验证
整数类型转换为_Bool类型、隐式类型转换
类型的转换是由运算符发起的
int main()
{
_Bool a,b,c,d;
a=0;// int 类型的0--》_Bool类型的0
b=1;//int 类型的1--》_Bool类型的1
c=100;//int 类型的100--》_Bool类型的1
d=1000000000000;//long long int 类型的100...0--》_Bool类型的1
/*
由于类型不一致-》类型转换-》自动进行的-》隐式类型转换
将整数类型转换为_Bool类型时
1. 如果数值在_Bool类型(0,1)存储范围内,只有类型会发生改变,数值不会发生改变
2. 如果数值不在_Bool类型存储范围内,类型和数值将全部发生改变,数值将被转换为1
*/
return 0;
}

整数类型转换为非_Bool类型、隐式类型转换
int main()
{
short int a=100;//int类型的100-》short int类型的100
int b=3700u;//unsigned int 类型的3700-->int类型的3700
//如果数值在新类型的存储范围之内,类型和数值全部不变
//转换为无符号整数
unsigned char c=-1;//int类型的-1 --》unsigned char类型的255
//unsigned char是一个字节大小
//因为-1太小了,需要加上一个数 -1+256=255
//256的来源->新类型最大存储值+1---》255+1
unsigned char d=257;//int类型的257-->unsigned char类型的1
//因为257太大了,需要减去一个数--》257-256=1
//256的来源->新类型最大存储值+1---》255+1
//注意:针对转换为有符号整数---》不同的C实现会有不同的处理结果
return 0;
}
整型转换阶、整型提升(重要)
整型转换阶

#include <stdio.h>
int main()
{
//类型的转换是由运算符发起的
signed char a=1,b=2;
//赋值运算符,将右操作数转换为左操作数一样的类型
++a;
//递增运算符,不改变操作数的类型
/*------------------------------------------*/
//其他的大多数运算符,都需要将操作数转换为相同的类型,然后再进行计算
//具体的转换规则:以加性运算符为例
a+380L;
/*
1. 整型提升:将转换阶低于int或者unsigned int的类型,转换为int或者unsigned int
2. 整型提升后,如果操作数类型(左右操作数)不一样,将转换低阶的操作数,转换为和高阶的操作数一样的类型
注意:1. 整型提升是对第一步的描述,和第二步无关
2. 整型提升以int 类型为首选
3. 当进行整型提升时,如果数值不在int类型的存储范围之内,那么就将它转换为unsigned int
*/
a+b;
int c=-20;
unsigned int d=1;
c+d;
//整型提升后,两个类型再同一阶,例如c+d,则将有符号类型转换为无符号类型
printf("%d\n",(c+d));//-19
printf("%u\n",(c+d));//4294967277
if((c+d)>0){//此时其实就是进入了将有符号类型转换为无符号类型,比较结果则非0直接进入
//进入这个验证,是不是很奇怪?
printf("大于0\n");
}else{
printf("小于等于0\n");
}
return 0;
}
如上图,发现,a和b即使类型相同,也需要先整型提升为int(首选)/unsigned int(int存储不下,则使用)然后参与运算,因为类型低于整型转换阶的分割线int/unsigned int。整型提升后,两个类型再同一阶,例如c+d,则将有符号类型转换为无符号类型;所以才会出现进入if而有别于肉眼计算的结果。
可参考:https://blog.csdn.net/elikang/article/details/85762104
负号运算符、负号表达式
运算规则:
- 如果操作数时一个整数,负号运算符会发起类型转换,对操作数进行整型提升
- 对操作数进行"取补码"的操作
- 取补码:0-1互转,然后+1
#include <stdio.h>
int main()
{
unsigned char a;
/*
因为有-需要进行整型提升, 1是整数,所以相当于什么也不变
然后取补码,1是int类型,再当前机器是4个字节,000...1 -> 111....0->111....1
又因为左操作数是 unsigned char类型,=操作时候会类型转换,设计前面三个字节
所以下面一步执行完毕,a内部实际存储的是255(八位全是1)
*/
a=-1;
/*
a++优先级更高,+=操作不改变类型,所以-操作的还是255(虽然有++操作导致的副作用,但是此处使用的是a++表达式而不是a)
然后-操作,因为是unsigned char所以首先整型提升,得到0000... 1111 1111,然后取补码得到111... 0000 0001
又因为=左右是signed char类型,舍弃前面三个字节,实际上就是0000 0001,结果为1
*/
signed char b=-a++;
printf("%d",b);//1
return 0;
}
显示类型转换
#include <stdio.h>
int main()
{
//显示/强制类型转换
//语法规则:(类型名)表达式
//()->转型运算符->将表达式的类型转换为括号中指定的类型
long long a;
a=33;
a=(long long int)33;//int->long long int
a=(long long)33+100;
a=(long long)(33+100);//(33+100) -》基本表达式
//()运算符的三种用法
//1. 函数调用运算符 2. 转型运算符 3. 构成基本表达式
return 0;
}
整数-指针的抓换、间接运算符的性质
间接运算符,根据指针(地址)对变量进行还原,它的操作数必须是指针,并且它不会发起类型转换
#include <stdio.h>
int main()
{
int a=0;
int b=(int)&a;
*(int *)b=100;
//Build failed: 1 error(s), 0 warning(s) (0 minute(s), 0 second(s)) ===|
// *b=100;
printf("%d",a);//100
return 0;
}
总结:*b=100
直接编译报错原因是,100不能赋值给指针类型,因为间接运算符不会自动发起类型转换;
但是如果加上(int *)
首先强制转换为指针类型,然后通过间接运算符拿到指针对应地址的值,实际上就相当于上面的a了;
所以最后100其实相当于赋值给a
include
-
include<>: 用尖括号包含头文件,在系统指定的路径下找头文件
-
include"": 用双引号包含头文件,先在当前目录下找头文件,找不到,在系统指定的路径下找头文件
inclue经常用来包含头文件,可以包含.c文件,但是尽量不要包含.c 因为include包含的文件会在预编译被展开,如果一个.c被包含多次,
展开多次,会导致函数重复定义。
注意:预处理只是对include等预处理操作进行处理并不会进行语法检查
define
定义宏用define,宏在预编译的时候进行替换
- 不带参宏
#define PI 3.14
- 在预编译的时候如果代码出现PI就用3.14替换
- 宏的好处:只要修改宏定义,其他地方在预编译的时候就会重新替换
- 宏定义的作用范围,从定义的地方到本文件末尾,如果想在中间终止宏的定义范围
#undef PI
- 带参宏
#define S(a,b) a*b
注意带参宏的形参a和b美哟类型名,S(2,4)将来在预处理的时候替换成实参替代字符串的形式,其他字符保留,2*4
S(2+4,3)会被替换成2+4*3
注意:带参宏,是在预处理的时候进行替换
- 解决歧义方法
如上2+4*3其实不是我们想要的,想解决如下处理
#define S(a,b) (a)*(b)
S(2+3,5)=>(2+3)*5
- 带参和带参函数的区别
- 带参宏被调用多少次就会展开多少次,执行代码的时候没有函数调用的过程,不需要压栈弹栈。所以带参宏,是浪费空间,因为被展开多次,节省了时间。
- 带参函数,代码只有一份,存在代码段,调用的时候去代码段取指令,调用的时候要压栈弹栈,有个调用的过程,所以带参函数浪费了时间,节省了空间,
- 带参函数的形参是有类型的,带参宏的形参没有类型名
选择性编译
- 形式一
#ifdef AAA
代码段一
#else
代码段二
#endif
如果在当前.c ifdef上边定义过AAA,就编译代码段一,否则二。注意和if else语句的区别,if else语句都会被编译,通过条件选择性执行代码而选折性编译,只有一块代码被编译
// AAA后面可以什么也没有
#define AAA
int main(int argc,char *argv[])
{
#ifndef AAA
printf("1");
#else
printf("11");
#endif
return 0;
}
- 形式二
和第一种互补,经常用在防止头文件重复包含
//注意此处是if n def
#ifndef AAA
代码段一
#else
代码段二
#endif
//例子一
#ifndef _MAX_H_
#define _MAX_H_
extern int max(int x,int y);
#endif
//例子二
#ifndef MYFUN_H
#define MYFUN_H
void myfun();
#endif // MYFUN_H
如上,如果某个C文件中,重复引入这个.h文件,则第一次会定义MAX_H则即使多次导入,后面的都无法通过
#ifndef _MAX_H_
,避免了预编译时候头文件重复展开
- 形式三
else的也可以省略
#if 表达式
代码段一
#else
代码段二
#endif
int main()
{
#if 1
//代码段一
#else
//代码段二
#endif
}
#define AAA 1
int main()
{
#if AAA
//代码段一
#else
//代码段二
#endif
}
- 宏函数
//宏函数需要添加小括号修饰,保证运算的完整性
//宏函数通常会将频繁,短小的函数,写成宏函数
//宏函数会比普通函数在一定程度上高,省去普通函数入栈,出栈时间上的开销
//优点:以空间换时间
// #define MYADD(x,y) x+y
#define MYADD(x, y) ((x) + (y))
int main()
{
//宏函数:实际上就是替换
printf("%d\n", MYADD(10, 20)); //30
// printf("%d\n",MYADD(10,20)*20); //410,使用注释宏函数的输出
//为了避免上面的情况,宏函数需要添加小括号修饰,保证运算的完整性
printf("%d\n", MYADD(10, 20) * 20);//600
return 0;
}
编译
- 动态编译
动态编译使用的是动态库文件进行编译
gcc hello.c -o hello
默认使用的就是动态编译
- 静态编译
静态编译使用的静态库文件进行编译
gcc -static hello.c -o hello
- 区别
- 使用库文件格式不一样(动态库和静态库)
- 静态编译要把静态库文件打包编译到可执行程序中,体积大
- 动态编译不会把动态库文件打包编译到可执行程序中,它只是编译链接关系,体积小,但是移动到其他电脑如果没有对应库则无法运行(因为依赖库没了,甚至移动exe地址相对路径变化都有可能导致依赖库找不到,然后无法使用)
- 但是动态库的话例如发布只需要在线更新替换就行,但是如果是静态编译的exe则需要整体替换安装,包太大;但是分发方便,各有优势
制作静态库
//把mylib.c制作成静态库
gcc -c mylib.c -o mglib.o
ar rc libtestlib.a mylib.o
//注意:静态库起名的时候必须以lib开头以.a结尾
//编译程序
//方式一:
gcc -static mytest.c libtestlib.a -o mytest
//方式二:
比如将libtestlib.a mylib.h 移动到/home/teacher下
mv libtestlib.a mylib.h /home/teacher
编译程序命令:
gcc -static mytest.c -o mytest -L/home/teacher -ltestlib -I/home/teacher
注意:
-L是指定库文件的路径
-l指定找哪个库,指定的只要库文件名lib后面的.a前面的部分
-I指定头文件的路径
//方式三:
可以将库文件以及头文件存放在系统默认指定的路径下
库文件默认路径是/lib或者是/usr/lib
头文件默认路径四/usr/include
sudo mv libtestlib.a /usr/lib
sudo mv mylib.h /usr/include
编译程序的命令:
gcc -static mytest.c -o mytest -ltestlib
制作动态库
//制作动态链接库
gcc -shared mylib.c -o libtestlib.so
//动态链接库的使用:三种方式和上面的静态完全一致,除了链接时找不到库需要特殊处理
当静态动态库重名且都在一个文件夹下面,则系统会优先使用动态库,或者加上-static指定使用静态库
网友评论