C语言学习笔记(1)
在学习前,需要有一定的C语言基础。不必很深入,只需要知道函数,头文件,指针,数组等的概念就可以,但并非0基础笔记。
由于写到后面,不好编辑了,决定分成多篇写,请按编号学习,或者直接看目录先。将会包括,c,c++,linunx_C系列
其他内容将放在入门到精通专栏里面
说明:该文章来自本人学习时的笔记,使用的编译器是Visual Studio,如有错误,可以在评论区或者私信我纠正,谢谢
1.前提知识
1.1项目结构
1.2 visual Studio导出模板
1.3预处理
预处理指令:以#开头的
转移符:\r
#include <stdio.h> //预处理
int main (void){ //C语言里面,标准必须加void表示不定形参,c++空括号不加表示void
printf("hello world\r"); // \反斜杠 /斜杆 \r转义序列
return 0;
}
预处理:
预处理:把头文件复制到文件里面
# include<stdio.h> //把头文件复制过去
宏定义:
# define N 5 //代码里面N会在预处理的时候被直接替换为5
宏函数
# define FOO(x) (1+(x)*(x)) //在预处理的时候会直接把宏函数内容替换
//整个表达式要用括号括起来,第一个左括号要贴紧宏函数,参数要用括号括起来
//调用宏函数没有调用函数的开销,可以避免简单重复的函数调用
//提供了一定的宏编程
多行宏函数:用\换行
#define SWAP(a,b){\
int t=a;\
a=b;\
b=t;\
}
1.4编译和链接
预处理、编译、汇编、链接
1.5进程的虚拟内存空间
内核 //和内核交互
栈 //管理函数调用
()
堆 //存放动态数据
数据段 //
代码段
1.6 注释
只在必要的时候写注释
2.变量
三要素:变量名、类型、值
2.1标识符
2.2关键字
在C语言里面有特殊含义的标识符
3.输入输出模型
3类资源:CPU、内存、外存
3.1 printf 格式化输出
%m.px //最少占有m个位置,在前面补空格 | 最少显示p个数字,不足的前面补0 | x表示输出格式
%-m.px //在后面补空格
3.2 scanf 格式化输入
%d //忽略前置空白字符,匹配一个十进制整数
%f //忽略前置空白字符,匹配一个浮点数
格式串 //精确匹配
空白字符 //匹配任意个空白字符
3.3scanf忽略空白字符
scanf("%c %c");//跳过空白字符匹配下一个非空白字符
3.4 putchar与getchar的使用
char c='A';
putchar(c);//输出c的字符
c=getchar();//用于输入一个字符
//惯用法
while(getchar=!='\n'){//跳过换行符后的剩余的字符
};
4.一些基本算法
4.1不分类
4.1.1求最大公约数
#求最大公约数
int gcd(int a, int b) {
if (a < b) {
SWAP(a, b);
}
while (b != 0) {
int r = a % b;
a = b;
b = r;
}
return a;
}
4.1.1判断是否为素数
函数的功能应该越单一越好,既然要判断一个数是不是素数,实现的功能只需要返回是或者不是,不要加其他多余的东西提高代码的复用率。
bool is_Prime(int n){
int rt = sqrt(n);
for(int i=2;i<=rt;i++){
if(n%i==0){
return false;
}
}
return true;
}
4.2位运算
4.2.1交换两个数的值(不用中间变量)
可以这么做,但不建议,代码可读性很差
//交换方法1:可能出现溢出问题
a=a+b;
b=a-b;
a=a-b;
//交换方法2:可读性很差
a=a^b;
b=a^b;
a=a^b;
4.2.2找数组中唯一的单独的一个数
Q:数组中有一个元素出现过一次,其余元素出现过两次,找这个单独的元素
int findSingleNum(int nums[],int n){
int singleNum=0;
for(int i=0;i<n;i++){
singleNum^=nums[i];//相同的数异或两次同一个数会变回原来的,
//唯一的单独数异或以后会存在singleNum中
}
return singleNum;
}
4.2.3找两个元素
Q:数组中有两个元素出现过一次,其余两个元素都出现过两次,找这两个单独出现的元素,可以按任意顺序返回答案
思路:
先求xor=a^b
xor正数与xor负数的补码表示上,在最低有效位上相差1
lsb即xor^(-xor)的值为xor的最低有效位为1,其他位为0的结果
a和b在这一位上一个为1一个为0
分类以后回归到2.2的题目
void findTwoNums(int nums[], int n) {
int Xor = 0;
for (int i = 0; i < n; i++) {
Xor ^= nums[i];//xor=a^b
}
int lsb = Xor ^ (-Xor); //a和b在这一位上不同
//根据这一位将所有元素分类
int a = 0, b = 0;
for (int i = 0; i < n; i++) {
if (nums[i]&lsb) {
a ^= nums[i];//相同的数一定会两次都和a或b相与,结果不变
} else {
b^=nums[i];
}
}
printf("两个数分别是 %d 和 %d \n",a,b);
}
5.编码
无符号整数(二进制编码)
有符号整数(补码)
Q:为什么用补码做有符号整数编码?
A:因为可以用加法达到减法的效果。
5.1 ASCII字符编码
5.1.1编码表
5.1.2大小写字母转换
int c=0x70;//P的编码
int tolower(int c);//把P转化为p
int toupper(int c);//把P转化为p
5.2字符转义序列
5.3数字转义序列
八进制(最多3位数字):‘\000’
十六进制转义序列:'\x000'
查看手册网址:C reference - cppreference.com
6.类型转换
6.1隐式转换
不同类型运算隐式转换,编译器会帮你自动转化为下一个等级
6.1.1整数提升
int -> long -> long long -> float -> double -> long double
6.1.2同一整数等级的转换
signed -> unsigned
提示:千万不要把无符号整数与有符号整数混合运算
6.2显示转换(强制转化)
int a=10;
float f;
f=(float)a;
6.3定义别名
Q的作用等价于int,重命名的目的是提高代码可读性
typedef的可移植性强,可以在前面定义别名的使用,只需要改变一次
比如int在桌面端占用4个字节,在嵌入式的时候可能就只有两个字节,只需要把重命名的前面的类型改变,比如嵌入式中int占用2字节,long占用4字节,只需要把int改为long就可以恢复桌面端占用4字节int的效果。
typedef int Q;//把int重命名为Q
Q a=0;//a是int类型
6.4sizeof计算长度
计算类型的时候sizeof时需要加上括号,本质上sizeof不是函数而是运算符。直接每次都加括号就好
int len=sizeof(int);//可以
printf("%d",len);
7.运算符
7.1赋值表达式
发生的过程为
i=3.14;//i被赋值为3
f=i;//f被赋值为3.00
int i;
float f;
f=i=3.14;//f的值为3.00,i的值为3
7.2自增
i++与++i
不必理会先自增还是后自增
把其理会为i=1,执行完语句以后副作用是会自增1
7.3位运算
7.3.1移位运算符
左移 <<,右移>>
在没有溢出的前提下,相当于左乘2,右除2
最好只对无符号整数进行移位运算
7.3.2按位运算符
~按位取反
&按位与,全1才1
|按位或,有1就1
^按位异或,同0异1
7.3.3位运算的性质
1.只能用于整数运算
2.有交换律和结合率
3.除了取反运算符都可以和等号结合变成复合运算符
i^=j等价于 i=i^1
7.4算术运算符
8.调试
8.1三种错误
1.编译错误:
C***:语法错误
2.链接错误:
LNK***:缺少头文件/函数名写错了
3.运行时错误:
运行时错误往往是逻辑错误,也就是需要调试,也就是常说的bug
8.2调试方式
1.打断点
2.调试在这边,左边的红色圆点是断点
3.开始调试以后,会直接运行到第一个断点,并把断点的上一行运行完,断点这行还没有运行
4.继续运行
点击继续会运行到下一个断点
5.逐语句运行
顾名思义,就是一条一条语句往下执行,点一下运行一条,遇到函数会跳到被调函数里面
6.逐过程运行
遇到函数会直接完成函数的过程直到下一个断点,不会跳到函数里面一行一行运行,而是直接完成函数的整个过程。
点击逐过程会直接完成Foo函数的运行,到下一个断点
7.跳出
在调试的过程中,发现函数没有问题,直接完成函数的运行跳出被调函数,回到主调函数里面
9.语句
9.1语句分类
表达式语句
选择语句:if switch
循环语句 :while do...while for
跳转语句:continue ,break , goto ,return
空语句: ;
复合语句:{ }
9.2 switch语句
解决的问题:效率低,可读性差,运行效率比if-else高
限制条件:
1.switch后面的表达式是必须是整型(char,枚举)
2.switch后面的表达式与标签case是用==比较的
switch (a) {
case 1:
//TODO
break;
case 2:
//TODO
break;
default:
//TODO
break;
}
注意事项:
1.可以多个case公用一组语句
2.会出现case穿透现象(在利用的时候,在下面要加上break through的注释,表示特意要让他穿透的)
switch (a) {
case 1:case 2://1和2都运行下面这个TODO
//TODO
break;
default:
//TODO
break;
}
10.数组
数组的单元叫元素,结构体的单元叫成员
10.1数组的内存模型
连续的一片的内存空间,有随机访问的性质。
数组的索引不是代表第几个元素,而是代表与第一个元素的偏移量
刻板印象:数组效率>链表效率
1.空间利用率
2.计算机按块读,数组的空间局部性好
10.2数组的定义
int arr[4];//声明一个数组
//变量名为arr
//类型为int[4]
10.3常数
const
11.随机
srand(time(null)):随机种子,一般只需要生成一次,一般这个数非常大,往往需要取余
rand:随机生成一个伪随机数
time:1970.1.1.00:00到现在的时间,要包括time.h
12.函数
12.1与数学函数的比较
数学:有返回值,没有副作用
C语言:可以没有返回值,可以有副作用
12.2准则
1.函数的功能应该越单一越好,函数的实现应该越高效越好
2.函数是c语言的基本构建组件,c语言的本质就是函数之间的调用,做开发的时候,要聚焦于函数,而不是细节
12.3函数相关概念
函数定义:void foo(int a){...}
函数声明:void foo(int a);
函数调用:foo(3);
函数指针:foo ,&foo
12.4值传递
栈帧中存入函数的参数,局部变量,返回地址
在值传递的时候,被调函数是无法修改主调函数的值,但是可以用指针解决
但不是所有值传递的时候,被调函数都无法修改主调函数的值,如果传递的值是数组,就可以修改
数组在参数传递的时候,会自我退化为一个指针,指向数组的第一个元素的指针,传递的第一个元素的地址,操作的还是原来的那个数组。
数组作为参数传递的时候,必须把长度传递过来,也不能使用SIZE宏来计算传递数组的长度。
Q:为什么说是数组退化为指针?
A:因为数组退化为指针以后,数组丢失了长度信息
Q:为什么这样设计?
A:1.避免大量数据的复制
2.被调函数可以操作主调函数的数组
3.让函数的调用更加灵活,比如传递一个长度为10的数组,直接对前面5个元素清零
12.5存储期限
存储期限:在运行时起作用,表示这个变量绑定的值在可以被引用的时间长度。
静态存储期限:内存中,数据段与代码段的部分,类似c++的生命周期,在进程启动时开始,在进程退出时消亡
自动存储期限:栈帧入栈时开始,栈帧出栈时消亡
动态存储期限:由程序员自己管理,malloc开始,free消亡
12.6局部变量与外部变量
局部变量:定义在函数里面的变量,只能在函数里面使用的变量
作用域:变量可以被引用的范围,或者说是在{...}块内的范围,(在运行时起作用)
块:简单说就是大括号{...}里面的全部叫一个块
存储期限:默认为自动存储期限(在栈里面),加上static会变成静态存储期限(在数据段里面),加了static初始化只执行一次。
外部变量:定义在函数外部的变量,也叫全局变量,但并非全局都可以使用。、
作用域:在外部变量的定义开始到这个文件的末尾,也可以叫文件作用域
存储周期:静态存储期限
静态存储期限的局部变量和外部变量的区别是作用域不同
Q:静态存储期限的局部变量有什么用?
A:存储上一次函数调用的状态
eg:使用静态存储期限的局部变量打印一个斐波那契
long long next_fib() {
static long long a = 0;
static long long b = 1;
long long t = a + b;
a = b;
b = t;
return a;
}
int main() {
printf("next_fib() = %lld\n", next_fib());
printf("next_fib() = %lld\n", next_fib());
printf("next_fib() = %lld\n", next_fib());
return 0;
}
12.7递归 recursion
12.7.1递归
递:将大问题分解为若干个子问题
子问题和大问题的求解方式一样,只是问题的规模不一样
归:将子问题合并为大问题的解
以斐波那契为例子,递归是指数增长级别的,求解方式的效率很低,会纯在大量重复的计算
long long fib(int n){
if (n==0||n==1){
return n;
}
return fib(n-1)+fib(n-2);
}
12.7.2动态规划
具有递归结构的问题不一定要用递归求解,可能纯在大量重复计算的问题,不一定要自下而上的求解,可以使用动态规划的方式去优化避免重复计算
继续以斐波那契为例子,使用动态规划
long long fib_dp(int n){
long long a=0;
long long b=1;
//循环不变式:每次进入循环体之前都成立的条件
for (int i =2;i<=n;i++){
long long t=a+b;
a=b;
b=t;
}
return b;
}
12.7.3循环不变式
循环不变式:每次进入循环体之前都成立的条件,要找到退出点,可以一直维持到循环语句的结束
可以用小的规模去计算得到正确的结果
控制表达式:上面i<=n那句叫控制表达式,要关注要不要写等号
12.7.4递归关键
递归公式:只考虑这一层与下一层的条件(使用数学归纳法)
边界条件:最后得到答案的条件
这个只能靠自己多刷题。
12.7.5要不要用递归——递归两问
1.找到问题的递归结构
2.要不要使用递归结构:存在重复结构(不用),问题缩减幅度很小(不用)
999.常见面试题
999.1题目清单
- 位运算
- 如何判断一个数是奇数
- 为什么数组的索引一般是从0开始的?
999.2位运算
1.如何判断一个数是奇数
错误做法:
i%2==1
当i为负数的时候,i的结果为-1,C语言的%和数学的mod是不一样的
C语言%运算的时候,余数的符号与被除数是一致的
可行做法:
i%2!=0
提示:取余,乘法,除法的运算符都是比较麻烦的,计算机操作的数都是二进制
正确做法:
i & 0x1==1;
建议使用16进制数,使用16进制表示关注的是二进制,而不是数本身
2.为什么数组的索引一般是从0开始的?
数组元素的地址计算公式为:i_addr = base_adddr + sizeof(elem_type)*(i-1)
数组的索引不是代表第几个元素,而是代表与第一个元素的偏移量