C语言
- 编译:
将.c源文件中的代码编译为对应的二进制指令。
`cc -c .c文件名`
如果一切正常的情况下,会生成一个.o文件,叫做目标文件
这个目标文件存储的是.c文件对应的二进制代码
>注意:编译器在编译的时候,先检查拟定的源文件中的代码是否符合C语言的语法规范,如果符合才能生成目标文件,如果不符合就会报错,不会生成目标文件
- 链接:
目标文件无法交给CPU直接执行,因为.o文件中只是存储的.c的二进制代码,一个可以被执行的文件中必须还要有执行代码才可以。链接的事情之一:为.o目标文件添加启动代码
`cc .o文件名`
如果一切正常的情况下,会生成一个a.out文件,是我们最终编写的程序,可以直接执行
如果程序中使用到了框架中的函数或者类.那么在链接的时候,就必须要告诉编译器 去那1个框架中找这个函数或者类.
`cc xx.o -framework 框架名称.`
- 执行
`./a.out`
补充:
1. 返回值代表程序的结束状态.
0 代表正常结束 非0代表非正常结束.
2. 参数
argv数组的第0个元素的值 是这个正在运行的程序的路径.
argc代表的是数组的长度.
3. 你要清楚一件事情.
我们写的程序可以在终端中运行. 运行的时候可以为程序传递一些数据.
传递的方式:
程序名 数据1 数据2 数据3 .....
在程序的内部如何拿到.
argc代表用户传递的数据的个数.
argv数组中每1个元素存储的就是用户传递过来的数据.
第0个元素是程序的路径.
- 数据类型
printf高级用法:
%md 位数不足m位,空格补足
%0md 位数不足m位,0补足
%.nf 四舍五入,保留n位小数,0补足(float)
%.nlf 四舍五入,保留n位小数,0补足(double)
赋值数据类型与变量声明类型不同,则会自动类型转换:
当变量的类型为int的时候:
1).赋值超出了int的范围,C系统会将数据转换为一个随机的int数据
2).赋值超出了int的范围太多,此时自动类型转换无能为力,编译器会直接报语法错误
3).赋值的数据类型是一个实型的小数,C系统会直接截取整数部分
当变量的类型为float的时候:
float a = 12.f;
1).赋值的数据是一个double类型的小数,C系统会将这个double类型的小数转换为float
1).赋值的数据是一个int类型的整数,C系统会将这个int类型的整数转换为float小数,直接加一个.0就可以
当变量的类型为double的时候:
double a = 12.0;
1).赋值的数据是一个float类型的小数,C系统会将这个float类型的小数转换为double,占据8个字节
1).赋值的数据是一个int类型的整数,C系统会将这个int类型的整数转换为double小数,直接加一个.0就可以
当变量的类型为char的时候:
char a = 'a';
ASCII码: 每一个字符数据都有一个与之对应的整数, A-65 a-97 0-48
为char变量赋值的时候,可以直接赋值ASCII码
char a = 97;
当我们为char变量赋值一个整数的时候,其实赋值的是以这个整数位ASCII码所对应的字符数据
- scanf函数的使用:
scanf("格式控制符", 变量地址列表);
步骤:
a.在格式控制符中使用占位符来要求用户输入一个指定类型的数据
b.在后面写上要将用户输入的数据存储在哪个变量的地址
使用&就可以获取到变量的地址
int num = 0;
scanf("%d", &num);
scanf函数的执行原理:
scanf函数是一个阻塞式的函数,当CPU执行到这个scanf函数的时候,CPU的执行就会暂停,不会继续往下执行了,并等待用户输入数据,当用户s输入完毕数据,按下回车键表示输入完毕,此时,就回将用户输入的数据赋值给后面指定的变量,然后再继续往下执行
// 注意:
1).scanf函数是输入不是输出,所以不要在后面加\n
2).scanf函数后面的参数是写变量的地址 而不是变量,使用&取变量的地址
3).如果程序正在运行,想要重新运行程序 要先将这个正在运行的程序停止
4).存储用户输入的数据的变量的类型要和scanf函数的占位符对应
用户在输入多个数据的时候,默认的分隔符号是空格或者回车
自定义分隔符:
在格式控制字符串中可以自定义多个输入数据的分隔符.
scanf("%d-%d-%d", &num, &boyNum, &avg);
代表输入3个数据,这三个数据用-分隔开
注意: 一旦指定了分隔符,那就必须使用指定的分隔符,空格回车则无效;一次输入的多个数据,只能是数(整型和浮点型),如果有char混合输入,就会出问题
数据输入完成后,并不是将这个数据直接赋值给变量,而是存入缓冲区
在执行scanf函数的时候,会先检查缓冲区中是否有数据,如果缓冲区中没有数据,那么就会让用户输入数据
当从缓冲区中拿数据的时候,如果要拿到的数据类型是整型或者实型,如果拿到的是空格 回车 Tab键 就会被自动忽略,继续往下拿
如果拿到的数据类型是字符型,不会忽略任何数据.
所以,当数字和字符混合输入的时候,字符的接收就有可能会出问题
解决方案: 在输入字符之前,将缓冲区中的数据全部清空
char color = "a";
rewind(stdin); // 将缓冲区中的数据全部清空
scanf("%c", &color);
- 算术运算符
1).算术表达式都有一个结果,一般处理方式是声明一个变量将这个表达式的结果存储起来
我们必须知道算术表达式的结果的类型,只有知道类型才可以声明一个对应的类型的变量来保存这个结果.
2).如果参与算术表达式的操作数的类型都是一致的,那么这个算术表达式的结果的类型就是这个l类型
10/4 记住,这个算术表达式的操作数都是int的,所以这个表达式的结果是2,不是2.5 ,所以正确的方式是使用int变量保存结果
如果为了得到结果是2.5, 可以将任意一个操作数的类型改为double(或乘以1.0)
10%3(求模运算)
可以用来判断1个数是否为另外一个数的倍数,或1个数能否被另一个数整除
实型数据无法参与求模运算,因为没有意义
m%n的结果一定在0-(n-1)之间
3).如果参与算术表达式的操作数的类型不一致,那么这个算术表达式的结果的类型就是范围最大的那个类型
int < float < double
4).当表达式中的操作数是一个char数据的时候,会先将这个char数据的ASCII码找出来代替,然后参与算术运算
- 自增自减运算
++i 先运算再赋值 i++ 先赋值再运算
--i I--
- 逗号表达式
int num = (i++,j++,++k,i+j+k);
从头到尾的去执行每一个子表达式,最后一个子表达式的结果就是整个表达式的结果
- 比较表达式
使用int类型的数据来表示真假
0代表假 非0代表真
- 逻辑运算符
断路问题:
int i = 0;
int res = i++ > 0 && ++i < 3
printf("%d", i);
打印结果 i=2 ????
逻辑表达式在执行的时候,是先计算左边的条件表达式的结果,再计算右边的表达式的结果
*****当是&&的时候,如果左边的不成立,则可以确定整个逻辑表达式的结果为0,而这个时候,右边的条件根本就不会去判断了,所以右边的代码也不会再执行
*****当是||的时候,如果左边的成立,则可以确定整个逻辑表达式的结果为1,而这个时候,右边的条件根本就不会去判断了,所以右边的代码也不会再执行
优先级:
! > && > ||
*/
- 函数
- goto函数; 不建议使用,因为它不安全,容易造成死循环,除非在特别确定的情况下不会造成d死循环,就可以使用
且只能在当前函数中跳转
取标签名下面的代码不能是声明代码,如果非要声明的话,可以先写个printf函数
loop:(标签名:)
printf("sss");
goto loop;
- 全局变量:
如果全局变量的类型是char类型,并且也没有初始化,那么系统就会自动给这个char变量赋值'\0'
\0 代表一个不可见的字符,这个字符的ASCII码就是0
全局变量与局部变量的异同点:
同: 都是变量,都是在内存中开辟一块空间来存储数据
异:1.声明的位置不同(函数内部与外部) 2.作用域不同
3.默认值不同
局部:默认值是一个垃圾数,是个随机数
全局:默认值为0
4.创建和回收的时间不同
1. 预处理指令/预处理代码
C语言的代码主要分为两类,
1).C代码 之前学习的代码都叫C代码
2).预处理代码 在编译之前执行的 以#开头的代码叫预处理代码
2. 手写一个C程序的步骤
1).在.c的源文件中写上符合C语言规范的源代码
2).编译 使用cc -c 指令将C语言的源代码编译为.o的目标文件
a.先检查源文件中的代码是否符合语法规范
YES 生成目标文件 NO 报错
3).链接 使用cc 指令 将目标文件链接生成一个可执行文件
a.为目标文件添加启动代码
4).执行可执行文件
.c源文件 ->执行.c文件中的预处理代码 ->检查语法 -> 编译成.o目标文件 ->链接生成可执行文件 ->执行
3.预处理指令
1).分类:
a.文件包含指令 #include
b.宏定义 #define
c.条件编译指令 #if
2).特点:
a.都是以#开头
b.预处理指令的后面没有分号
c.在编译的时候,在检查语法之间执行
4. 文件包含指令 #include
1).作用:将指定的文件的内容拷贝到指令的地方
2).语法:
#include "文件路径" 直接目录查找
#include <文件路径> 从编译器目录查找
- int num = 11;
if ((num & 1) == 0) {
num是偶数
}
- 数组:
1).特点:
a.可以存储多个数组
b.存储的多个数据的类型相同
c.长度固定
d.存储在数据中的数据方便管理
2).语法:
元素类型 数组名[长度];
int arr[2];
arr的类型是int数组
3).存储/取出 数据,使用下标
4).遍历
5).注意点:
a.数组的长度
b.数组元素的默认值 0
c.初始化
int arr[] = {10,293,10};
int arr[] = {0};
当声明数组的同时使用大括弧初始化数组的时候 数组的长度不能是一个变量
3.数组在内存中的存储
数组的地址
数组名 == 数组地址 == 数组的低字节的地址 == 数组的第0个元素的地址 == 数组的第0个元素的低字节地址
数组的长度
sizeof(数组名) / 每一个元素占用的字节数
sizeof(arr) / sizeof(arr[0])
4.数组与函数
当数组作为函数的参数的时候
传递实参数组的时候会丢失数组的长度,所以在函数的内部使用sizeof计算数组的长度的时候是计算不出来的
再多传一个参数 让调用者将数组的长度一并传递过来
5.数组的算法
a.求最大,最小值,累积和.平均值
b.判断指定的数据是否包含在数组中
c.找出指定的数据在数组中的下标
d.产生不重复的随机数
int balls[] = {0};
for (int i = 0; i < 6; i++) {
loop:
printf("");
int num = arc4random_uniform(33)+1;
int rtes = containsObject(balls, 6, num);
if(rtes == 1) {
goto loop;
}else {
balls[i] = num;
}
}
int containsObject(int arr[], int length, int num) {
for (int i = 0; i < length; i++) {
if (arr[i] == num) {
return 1;
}
}
return 0;
}
e.
// 选择排序
for (int i = 0; i < len - 1; i++) {
for (int j = i + 1; j < len; j++) {
if (arr[i] < arr[j]) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
// 冒泡排序
for (int i = 0; i < len - 1; i++) {
for (int j = 0; j < len - 1 - i; j++) {
if (arr[j] < arr[j+1]) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
二分查找:
// 前提:arr是一个有序数组
int min = 0;
int max = len - 1;
int num = 11;
while(min<max) {
int mid = min+max/2;
if (arr[mid] == num) {
return num;
} else if(arr[mid] > num) {
max = mid-1;
} else {
min = mid+1;
}
}
- 二维数组与函数
1).当二维数组作为函数的参数的时候,丢失行数和列数, 应同时传入行数和列数
2).形参二维数组: 行数可以省略 列数不可省略
实现二维数组: 行数任意 但是列数需保持一致
void test(int rows, int cols, int arr[][cols]);
- 字符串
1).字符串数据的每一个字符存储在字符数组中,会自动追加一个'\0'结束
char name[] = "jack"; ======= char name[5]
2).用printf("%s", name); 输出字符串
3).输入字符串
scanf("%s", name);
a.不安全,当数组的长度不够的时候就会崩溃
b.空格问题, 输入空格会有\0问题
4).长度计算
a.不能用sizeof字符数组计算,因为这样得到的是字符数组的长度
b.从第一个字节开始计数,直到遇见'\0'为止
sizeof 返回定义arr数组时,编译器为其分配的数组空间大小,不关心里面存了多少数据。
strlen 只关心存储的数据内容,不关心空间的大小和类型。
5).字符串的常用函数
puts(); // 输入 不安全
gets(); // 输出 不安全
strlen(); // 长度
strcmp(); // 比较两个字符串设这两个字符串为str1,str2,若str1=str2,则返回零
strcpy(); // 把str2的字符串复制到str1中,这两个都是地址
strcat(); // 拼接字符串
- 指针变量:专门用来存储地址的变量, 专门用来存储另外一个变量的地址的,可以说指针变量指向了另外一个变量
变量的地址就叫做指针, 指针就是地址, 地址就是指针
int num = 20;
int *p1 = #
p1操作的是p1这个指针变量,可以取p1的值, 也可以为p1赋值
&p1拿到的是p1的地址
- 野指针: 没有初始化的指针变量,值为垃圾值,随机数
访问野指针指向的变量会报错,BAD_ACCESS错误
- NULL 代表指针变量不指向内存中的任何地址,NULL完全等价于0,所以也可以赋值0
int *p = NULL;
int *p = 0;
此时依然不可以访问,否则会报错
函数的参数:
1.当函数的参数类型是int 等等普通类型的时候,
参数传递是值传递
在函数的内部改变形参变量的值,对实参变量没有丝毫影响
2.当函数的参数类型是数组的时候,
参数传递是地址传递
在函数的内部改变参数数组的元素的时候,其实改变的就是实参数组的元素
3.当函数的参数类型是指针的时候,
在函数的内部访问参数指针指向的变量的时候,其实改变的就是实参
无论指针是什么类型,在内存中都占据8个字节
- 多级指针
声明二级指针: **p
三级指针: ***p
// 指针遍历数组
int arr[7] = {10,20,30,40,50,60,70};
int *p = arr;
for (int i = 0; i < 7; i++) {
printf("%d\n", *(p+i));
printf("%d\n", *(arr+i));
}
- 当数组作为函数的参数的时候
在声明这个参数数组的时候,它不是去创建1个数组,而是去创建一个用来存储地址的指针变量
如果我们为函数写了一个数组作为参数
其实编译器在编译的时候已经把这个数组换成了指针
所以,在声明参数的时候不是创建数组,而是创建一个存储数组地址的指针变量
这 也是为什么我们通过sizeof计算参数数组得到的永远都是8
void test(int arr[], int len);
替换为 void test(int *arr);
- 索引的本质
1).指针变量可以使用中括弧,在中括弧中写上下标来访问数据
2).p1[n]; 前提是p1是一个指针变量
完全等价于 *(p1+n)
3).只要是指针都可以使用中括弧下标,就相当于是访问指针指向的变量
arr[0] = 100;// *(arr+0)
arr[1] = 200;// *(arr+1)
两个指针变量相减 就是用在数组中,判断两个元素之间相差多少个元素
- 字符串的两种存储方式
1).使用字符数组来存储
char name[] = "jack";
2).使用字符指针存储
char *name = "jack";
区别:
1).存储的结构不同
2).可变与不可变
- 字符串的恒定性
- fputs(); f-->File
作用: 将字符串数组 输出到 指定的流中
流: 标准输出流->控制台
文件流->磁盘上的文件
使用格式:
fputs(要输出的字符串, 指定的流);
1.标准输出流(控制台) : stdout
char *name = "sds";
fputs(name, stdout);
2. 将字符串存储到文件中
a.要先声明一个文件指针,指向磁盘上的文件
fopen函数可以创建一个指向文件的指针
fopen(文件的路径(代表创建的指针指向这个文件), 操作文件的模式(对文件做什么操作));
操作文件的模式: "w"-->write
"r"-->read
"a"-->append 追加数据
当操作模式是w的时候,如果文件不存在则创建这个文件
如果文件存在则会将原来的文件替换掉
当操作模式是a的时候,如果文件不存在则创建这个文件
如果文件存在则会追加
b.使用fputs函数将字符串写入到指定的文件流中
fputs(字符串, 文件指针);
c.写完之后,一定要使用fclose函数结束这个文件
FILE* pFile = fopen("/User/Desktop/abc.txt", "w");
char *name = "ss";
fputs(name, pFile);
fclose(pFile);
printf("写入成功");
- fgets函数
作用: 从指定的流中读取字符串
fgets(要将字符串存储到哪一个数组中, 最多接收多少个长度的字符串(n , 最多n-1个,最后一个自动\0), 文件的路径);
1.标准输出流(控制台) : stdin
char input[5];
fgets(input, 5, stdin);
size_t len = strlen(input);
if (input[len - 1] == '\n') {
input[len - 1] = '\0';
}
2. 将字符串存储到文件中
FILE* pFile = fopen("/User/Desktop/abc.txt", "r");
char name[50];
fgets(name, 50, pFile);
fclose(pFile);
printf("写入成功");
- 字符串数组
1).字符的二维数组 char name[][4]
2).字符指针数组 char *name[4]
- const关键字
用来修饰变量,被const修饰的变量具备一定程度上的不可变性,叫做"只读变量"
const int num = 10;
const int arr[4] = {10,20,30,40}; 数组的元素不可更改
const int *p1 = # 无法通过指针p1去修改指针指向的变量的值,但是如果直接去操作变量是可以的,
但是指针变量的值可以改,可以把另一个变量的地址赋值给指针
*p1 = 100; ❎
int age = 20;
p1 = &age; ✅
int const *p1 = # 效果同上
int * const p1 = #
p1的值不能修改,但是可以通过p1去修改p1指向的变量的值
int const * const p1 = #
p1的值不能修改,也不可以用p1去修改p1指向的变量的值
使用场景:
1).const的特点:
被const修饰的变量,是只读变量,只能取值,不能改值
所以,const变量的值至始至终都不会发生变化
2).当某些数据是固定的,在整个程序运行期间都不会变化,而且也不允许他人去更改,此时就用const
3).当函数的参数是一个指针的时候,函数的内部有可能会改变实参变量的值
void test(const int arr[]);
- 内存管理
1.内存的五大区域
栈: 局部变量
堆: 堆区中的字节空间允许程序员是手动的申请
BSS段: 未初始化的全局变量和静态变量
数据段: 已初始化的全部变量,静态变量,常量数据
代码段: 存储代码段落
2.如何向堆区申请字节空间来使用
1).我们在堆区中申请的字节空间,如果我们不主动释放,那么系统是不会释放的,除非程序结束了
2).在堆中申请字节空间的步骤: 申请->使用->释放
#include <stdlib.h>
- 向堆内存中申请连续的参数个数字节空间:
malloc(n);
(size_t == unsigned long)
返回值: void * 代表没有类型的指针
返回的是创建的空间中第一个字节的地址,地址是没有类型的
* 在堆区申请的字节空间是从低地址向高地址分配
每次申请的字节地址都是从0开始,每一次申请的字节空间不一定挨在一起
但是,每一次申请的指定个字节一定是连续的
* 在堆区申请的字节,里面是有值的,值是垃圾值,不会自动清零
* 在堆区申请字节空间的时候有可能会申请失败,失败时返回NULL值,最好判断一下
int *p1 = malloc(4);
if(p1) {
// 申请成功
}
* 申请的空间使用完毕后,一定要记得释放
释放申请的堆空间: free(指针);
- 向堆内存中申请指定个数字节空间:
calloc(多少个单位, 每一个单位多少字节);
int *p1 = calloc(3, sizeof(int));
if (p1) {}
相较malloc,申请完之后,系统会将字节中的数据清零
- realloc 扩容
如果原来的剩余空间不够扩容,则拷贝数据后自动释放,重新找一块足够的空间申请
- 指向函数的指针
1).可以定义一个指针指向一个函数,来使用这个指针间接调用这个函数
2).指向函数的指针的声明
拷贝函数头,去掉函数名 用小括弧代替,里面写上 *指针名
3).初始化
函数名就代表函数的地址,直接将函数名赋值给指针
函数声明:
void text () {
}
改为:
void (*pFunc) () = test;
4).使用
a. pFunc();
b. (*pFunc)();
- 结构体
作用:创建一个数据类型,这个数据类型的变量是由多个其他普通类型的小变量联合而成的
struct 新类型名称(首字母x大写)
{
成员;
};
struct Student
{
char *name;
int age;
};
声明结构体类型的变量:
struct 新类型名称 变量名
结构体变量的初始化:
struct Student std1;
std1.name = "jack";
std1.age = 18;
也可以: 按顺序初始化成员变量
struct Student std1 = {"jack", 18};
也可以:
struct Student std1 = {.age = 18, .name = "jack"};
结构体成员变量的默认值:
声明一个结构体变量,如果没有为这个结构体变量的成员赋值,那么成员有值,是垃圾值
只要在声明结构体变量的同时只要初始化一个成员,其他的成员就会被自动初始化为0
作用域:一般情况下,都是定义在函数外面
结构体变量之间的相互赋值:
相同结构体绝对可以: 将源结构体中的每一个变量拷贝一份赋值给目标结构体
struct Student std1 = {"jack", 18};
struct Student std2 = std1;
- 结构体数组
struct 结构体类型名称 数组名称[数组长度];
struct Student arr[5];
arr[0] = (struct Student){"jack", 18};
或:
struct Student arr[5] = {{"jack", 18},
{"jack", 18}
};
结构体数组长度计算:
sizeod(arr) / sizeof(struct Student);
结构体指针的声明:
struct 结构体类型名称 * 指针名;
struct Student * pStu1 = &std1;
指针间接访问结构体变量的值:
(*pStu1).age = 19;
pStu1->age = 19;
结构体嵌套:
struct Student
{
char *name;
int age;
struct Date birthday;
};
struct Date
{
int year;
int month;
int date;
}
struct Student stu1 = {"jack", 19, {2000, 10, 20}};
结构体作为函数的参数: "值传递"
int isAdult(struct Student stu){
if (stu.age >=18)
return 1;
}else {
return 0;
}
结构体作为函数的返回值:
struct Student getAStudent(){
struct Student stu1 = {"jack", 19, {2000, 10, 20}};
return stu1;
}
// 返回指针
struct Student *getAStudent(){
struct Student *p1 = calloc(1, sizeof(struct Student));
p1->name = "jack";
...
return p1;
}
- 枚举
创建一个数据类型,这个数据类型的变量的取值被限定
enum 新类型
{
枚举值1, 枚举值2..... (每一个枚举值都有一个对应的整数,从0开始,依次递增)
};
enum ButtonType
{
ButtonTypeNormal,
ButtonTypePress,
ButtonTypeDisabled
};
声明枚举类型的变量:
enum 新类型名称 变量名
enum ButtonType type = ButtonTypeNormal;
- typedef 将一个已经存在的数据类型取一个别名
- 预处理指令
#define 与 typedef 的区别:
1).#define是一个预处理指令,在预编译的时候执行,把宏名换成宏值
typedef是一个C代码,在运行的时候才会执行
2).#define可以将任意的C代码取一个标识名
typedef只能为数据类型取名
#if
#elseif
#endif
#ifdef
#ifndef
- static修饰局部变量
1).修饰局部变量,这个变量就叫做静态变量
2).静态变量不再存储在栈区域,而是存储在常量区
3).第一次执行这个函数的时候,就会将这个静态变量声明在常量区,当函数执行完毕后,这个静态变量不会被回收,仍然存在
后面再去执行的时候,声明静态变量的这句话会直接略过不会再执行
- extern不能修饰局部变量
- static修饰全局变量\函数:只能在当前模块中使用
- extern修饰全局变量\函数:可以跨模块使用
C语言笔记
©著作权归作者所有,转载或内容合作请联系作者
- 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
- 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
- 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...