目录
- 一、scanf和getchar
- 二、产生随机数函数
- 三、辗转相除法求最大公约数
- 四、函数的参数
- 4.1 实际参数(实参)
- 4.2 形式参数(形参)
- 4.3 内存分配
- 五、函数的调用
- 5.1 传值调用
- 5.1 传址调用
- 六、函数的声明和定义
- 6.1 函数的声明
- 6.2 函数的定义
- 6.2 静态库的导出
- 七、函数的递归
- 7.1 递归的两个必要条件
- 7.2 练习
- 八、数组
- 九、操作符
- 9.1 操作符的属性
- 9.2 隐式类型转换
- 9.2.1 整型提升
- 9.2.2 算术转换
- 十、指针
- 10.1 野指针成因
- 10.2 规避野指针
- 10.3 指针运算
- 10.4 指针数组
- 十一、结构体
- 十二、寄存器
一、scanf和getchar
getchar()读取一个字符,读取成功返回字符的ASCII码值,返回类型为int。如果读取失败会返回EOF(EOF end of file 文件结束标志,#define EOF -1) (ctrl+z——会让scanf或者getchar返回EOF)
putchar()打印字符
此时scanf默认读取\n之前的所有字符。所以第一次scanf直接读取到了123456,而第二次scanf则读取到了\n
此时scanf默认读取的是空格前面的,所以第一次读取123456。所以为了让scanf能读到\n后面自己输入的数据就可以用循环getchar的方式进行读取
fflush(stdin)这个函数的功能是清理输入缓冲区中的数据,但是不一定有用,因为在新版本的VS上这个函数的功能被取消了。
sleep(1000)单位是毫秒,1000毫秒等于1秒
system(“cls”) //system函数可以执行系统命令,cls是清理屏幕
两个字符串比较相等是不能使用==的,应该使用strcmp库函数
strcmp返回0表示2个字符串相等(strcmp(‘a’,‘a’))
strcmp返回>0的数字,表示一个字符串大于第二个字符串
strcmp返回<0的数字,表示一个字符串小于第二个字符串
scanf函数读字符串的时候会自己在后面加上\0,即当定义字符串为xxxxxx,当输入为123回车时,则输出就为123。在内存中可以看出,字符串123后面是\0
二、产生随机数函数
rand() 可以生成随机数,随机数的范围是:0-32767,返回的是整数
time函数可以返回一个时间戳(中间传个空指针就行了)
srand(unsigned int)time(NULL) 要给srand传递一个变化的值,计算机上的时间是时刻发生变化的
生成0-99的数为:rand()%100+1
shutdown -s -t 60 是系统中关机的命令,表示60秒后进行关机 (在程序中则使用system(“shut down -s -t 60”))
shutdown -a 则表示取消关机(在程序中则使用system(“shut down -a”))
三、辗转相除法求最大公约数
m n k
24 18 6
18 6 0
所以输出公约数n为6
四、函数的参数
4.1 实际参数(实参)
真实传给函数的参数,叫实参。
实参可以是:常量、变量、表达式、函数等。
无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
4.2 形式参数(形参)
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。
4.3 内存分配
栈区:局部变量,形式参数
堆区:动态内存管理,malloc,realloc,calloc,free
静态区:静态变量,全局变量
C语言中,内存主要分为5个区,分别为栈区、堆区、全局/静态存储区、常量存储区、代码区。其中代码区存放源程序的二进制代码,其余四个区都存储进程运行过程中需要的存储的变量。
(1)栈
存局部变量、函数,调用函数时会开辟栈区,函数结束时就自动回收,遵循后进先出的原则,从高地址向低地址增长。
(2)堆
malloc、realloc、calloc等开辟的内存就在堆,从低地址向高地址增长,由程序员分配和释放,系统步自动回收,所以一定要记得申请了就要释放,以免溢出。
系统提供了四个库函数来实现内存的动态分配:
①malloc(size) 在内存的动态存储区中分配一个长度为size的连续空间。
②calloc(n,size) 在内存的动态存储区中分配n个长度为size的连续空间。
③free§ 释放指针变量p做指向的动态空间。
④realloc(p,size) 将指针变量p指向的动态空间大小改变为size。
(3)全局(静态)区
通常是用于那些在编译期间就能确定存储大小的变量的存储区,但它用于的是在真个程序运行期间都可见的全局变量和静态变量。
(4)常量区
字符串、数字等常量存放在常量区。const修饰的全局变量存放在常量区。程序运行期间,常量区的内容不可以被修改。
(5)代码区
程序执行代码存放在代码区,其值不能修改(若修改则会出现错误)。字符串常量和define定义的常量也有可能存放在代码区。
五、函数的调用
5.1 传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
5.1 传址调用
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
六、函数的声明和定义
6.1 函数的声明
- 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。
- 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
- 函数的声明一般要放在头文件中的。
6.2 函数的定义
函数的定义是指函数的具体实现,交待函数的功能实现。
6.2 静态库的导出
首先右键项目名称,选择最后一个属性,然后在配置属性的常规中找到配置类型,将原来你的应用程序(.exe)改成静态库(.lib)就行了。
当我们拿到.lib和.h文件时就可以对静态库进行导入执行了。
//以简单的加法程序为例
//其中Add函数的实现为
//int Add(int x , int y)
{
return x+y;
}
#include "add.h" //首先头文件还是需要包含声明
//.lib - 静态库
//导入静态库
#pragma comment(lib,“add.lib”) //对静态库进行导入
int main() //此时就可以在程序中进行调用了
{
int a=0;
int b=0;
//输入
scanf("%d %d",&a,&b);
//加法
int c=Add(a,b); //函数调用
//打印
printf("%d\n",c);
return 0;
}
七、函数的递归
7.1 递归的两个必要条件
存在限制条件,当满足这个限制条件的时候,递归便不再继续。
每次递归调用之后越来越接近这个限制条件。
7.2 练习
1.接收一个整型值(无符号),按照顺序打印它的每一位
例如:
输入:1234,输出 1 2 3 4
void print(unsigned int n)//1234
{
if (n > 9)
{
print(n / 10);
}
printf("%d ", n % 10);
}
int main()
{
unsigned int num = 0;
//输入
scanf("%d,num");
print(num);
return 0;
}
2.编写函数不允许创建临时变量,求字符串的长度
int my_strlen(char* s)
{
if (*s == '\0')
return 0;
else
return 1 + my_strlen(s + 1);
}
int main()
{
char arr[] = "abc";
//a b c \0
int len = my_strlen(arr);
printf("%d\n", len);
return 0;
}
3.求n的阶乘。
int Fac(int n)
{
if (n <= 1)
return 1;
else
return n * Fac(n - 1);
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Fac(n);
printf("%d\n", ret);
return 0;
}
4.求第n个斐波那契数
//递归的方式
//int Fib(int n)
//{
// if (n <= 2)
// return 1;
// else
// return Fib(n - 1) + Fib(n - 2);
//}
//迭代的方式
int Fib(int n)
{
int a = 1;
int b = 1;
int c = 1;
while (n >= 3)
{
c = a + b;
a = b;
b = c;
n--;
}
return c;
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Fib(n);
printf("%d\n", ret);
return 0;
}
八、数组
数组名该怎么理解?
数组名通常情况下就是数组首元素的地址
但是有两个例外:
1.sizeof(数组名),数组名单独放在sizeof()内部,这里的数组名表示整个数组,计算的是整个数组的大小
2.&数组名,这里的数组名也表示整个数组,这里取出的是整个数组的地址
除此之外所有遇到的数组名都表示数组首元素的地址
数组传参,形参可以是指针,也可以是数组的形式
但是形参写成数组是为了方便理解,本质上还是指针
那形参的地方,会不会创建新的数组呢? 不会的!
以递归的形式实现字符串的逆序
void reverse_string(char* s)
{
int len=strlen(s);
char tmp=s[0];
s[0]=s[len-1];
s[len-1]='\0';
if(strlen(s+1)>=2)
reverse_string(s+1);
s[len-1]=tmp;
}
int main()
{
char arr[]="abcdefg";
reverse_string(arr);
printf("%s\n",arr);
return 0;
}
九、操作符
右移:
算数右移(右边丢弃,左边补原来的符号位)
逻辑右移(右边丢弃,左边直接补0)
C语言没有明确规定到底是算术右移还是逻辑右移,一般编译器上采用的是算术右移
9.1 操作符的属性
复杂表达式的求值有三个影响的因素:
1、操作符的优先级
2、操作符的结合性
3、是否控制求值顺序
操作符 | 描述 | 用法示例 | 结果类型 | 结合性 | 是否控制求值顺序 |
---|---|---|---|---|---|
() | 聚组 | (表达式) | 与表达式同 | N/A | 否 |
() | 函数调用 | rexp (rexp, …,rexp) | rexp | L-R | 否 |
[] | 下标引用 | rexp[rexp] | lexp | L-R | 否 |
. | 访问结构成员 | lexp.member_name | lexp | L-R | 否 |
-> | 访问结构指针成员 | rexp->member_name | lexp | L-R | 否 |
++ | 后缀自增 | rexp++ | lexp | L-R | 否 |
– | 后缀自减 | lexp – | rexp | L-R | 否 |
! | 逻辑反 | ! rexp | rexp | R-L | 否 |
~ | 按位取反 | ~ rexp | rexp | R-L | 否 |
+ | 单目,表示正值 | + rexp | rexp | R-L | 否 |
- | 单目,表示负值 | - rexp | rexp | R-L | 否 |
++ | 前缀自增 | ++ lexp | rexp | R-L | 否 |
– | 前缀自减 | – lexp | rexp | R-L | 否 |
* | 间接访问 | * rexp | lexp | R-L | 否 |
& | 取地址 | & lexp | rexp | R-L | 否 |
sizeof | 取其长度,以字节表示 | sizeof rexp sizeof(类型) | rexp | R-L | 否 |
(类型) | 类型转换 | (类型) rexp | rexp | R-L | 否 |
* | 乘法 | rexp * rexp | rexp | L-R | 否 |
/ | 除法 | rexp / rexp | rexp | L-R | 否 |
% | 整数取余 | rexp % rexp | rexp | L-R | 否 |
+ | 加法 | rexp + rexp | rexp | L-R | 否 |
- | 减法 | rexp - rexp | rexp | L-R | 否 |
<< | 左移位 | rexp << rexp | rexp | L-R | 否 |
>> | 右移位 | rexp >> rexp | rexp | L-R | 否 |
> | 大于 | rexp > rexp | rexp | L-R | 否 |
>= | 大于等于 | rexp >= rexp | rexp | L-R | 否 |
< | 小于 | rexp < rexp | rexp | L-R | 否 |
<= | 小于等于 | rexp <= rexp | rexp | L-R | 否 |
== | 等于 | rexp == rexp | rexp | L-R | 否 |
!= | 不等于 | rexp != rexp | rexp | L-R | 否 |
& | 位与 | rexp & rexp | rexp | L-R | 否 |
^ | 位异或 | rexp ^ rexp | rexp | L-R | 否 |
| | 位或 | rexp | rexp | rexp | L-R |
&& | 逻辑与 | rexp && rexp | rexp | L-R | 是 |
|| | 逻辑或 | rexp | rexp | rexp | |
? : | 条件操作符 | rexp ? rexp : rexp | rexp | N/A | 是 |
= | 赋值 | lexp = rexp | rexp | R-L | 否 |
+= | 以…加 | lexp += rexp | rexp | R-L | 否 |
-= | 以…减 | lexp -= rexp | rexp | R-L | 否 |
*= | 以…乘 | lexp *= rexp | rexp | R-L | v |
/= | 以…除 | lexp /= rexp | rexp | R-L | 否 |
%= | 以…取模 | lexp %= rexp | rexp | R-L | 否 |
<<= | 以…左移 | lexp <<= rexp | rexp | R-L | 否 |
>>= | 以…右移 | lexp >>= rexp | rexp | R-L | 否 |
&= | 以…与 | lexp &= rexp | rexp | R-L | 否 |
^= | 以…异或 | exp ^= rexp | rexp | R-L | 否 |
= | 以…或 | lexp | = rexp | rexp | R-L |
, | 逗号 | rexp,rexp | rexp | L-R | 是 |
其中,lexp表示左值表达式,rexp表示右值表达式。L-R表示从左到右进行结合(从左到右进行计算),R-L表示从右到左进行结合(从右到左进行计算)
交换两个值,不允许创建临时变量
int main()
{
int a = 3;
int b = 5;
printf("交换前:a=%d b=%d\n", a, b);
a = a ^ b;
b= a ^ b;
a = a ^ b;
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
sizeof
int main()
{
int arr[10] = { 0 };
printf("%d\n", sizeof(arr));//40-计算整个数组的大小,单位字节
printf("%d\n", sizeof(int [10]));//40
return 0;
}
#include <stdio.h>
void test1(int arr[])
{
printf("%d\n", sizeof(arr));//(2) int*类型 在32位下是4个字节,64位下是8个字节
}
void test2(char ch[])
{
printf("%d\n", sizeof(ch));//(4) char*类型 在32位下是4个字节,64位下是8个字节
}
int main()
{
int arr[10] = { 0 };
char ch[10] = { 0 };
printf("%d\n", sizeof(arr));//(1) 10*4=40
printf("%d\n", sizeof(ch));//(3) 10*1=10
test1(arr);
test2(ch);
return 0;
}
统计一个二进制数中1的个数
int number_of_1(int m)
{
int count = 0;
while (m)
{
m = m & (m - 1); //每执行一次就相当于去掉一个1
count++;
}
return count;
}
int main()
{
int n = 0;
scanf("%d", &n);//15
int ret = number_of_1(n);
printf("%d\n", ret);//4
return 0;
}
9.2 隐式类型转换
9.2.1 整型提升
C的整型算术运算总是至少以缺省整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度
一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值(主要针对char和short类型),都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。
如何进行整体提升呢?
整形提升是按照变量的数据类型的符号位来提升的
//负数的整形提升
char c1 = -1;
变量c1的二进制位(补码)中只有8个比特位:
1111111
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为1
提升之后的结果是:
11111111111111111111111111111111
//正数的整形提升
char c2 = 1;
变量c2的二进制位(补码)中只有8个比特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为0
提升之后的结果是:
00000000000000000000000000000001
//无符号整形提升,高位补0
//计算到最后如果存储字节内存不够则会发生截断(即从最小位开始取相应的字节数)
9.2.2 算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
long double
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。(向上转换,即从int向long double进行转换)
十、指针
指针类型可以决定指针解引用的时候访问多少个字节(指针的权限,short* -->2 float* --> 4)
指针类型决定指针+1操作时的步长是多少(整型指针+1跳过4个字节,字符指针+1跳过1个字节)
10.1 野指针成因
1.指针未初始化
2.指针越界访问
3.指针指向的空间释放
10.2 规避野指针
1.指针初始化
2.小心指针越界
3.指针指向的空间释放及时置NULL
4.避免返回局部变量的地址
5.指针使用之前检查有效性
10.3 指针运算
(1)指针±整数:表示指针指向空间向前向后走多少步(整型指针+1跳过4个字节,字符指针+1跳过1个字节)
(2)指针-指针:得到的数值的绝对值,是指针和指针之间的元素个数
指针-指针运算的前提是:指针和指针指向了同一块空间。
(3)指针的关系运算:地址是有大小的,指针的关系运算就是比较指针的大小
数组与指针的联系
数组中,数组名其实是数组首元素得到地址,数组名==地址==指针
当我们直到数组首元素地址的时候,因为数组又是连续存放的,所以通过指针就可以遍历访问数组,数组是可以通过指针来访问的。(数组的类型随着个数的不同类型是不同的,比如 int [10]和int [8])
10.4 指针数组
int main()
{
char arr1[] = "abcdef";
char arr2[] = "hello world";
char arr3[] = "cuihua";
char* parr[] = { arr1,arr2,arr3 };
char** p = parr;
//字符类型的指针数组直接用%s就可以打印了, printf("%s ",parr[i])
return 0;
}
int main()
{
char arr1[] = {1,2,3,4,5};
char arr2[] = {2,3,4,5,6};
char arr3[] = {3,4,5,6,7};
char* parr[] = { arr1,arr2,arr3 };
//整型类型的指针数组需要用[]进行访问,printf("%d ",parr[i][j])或printf("%d "*(parr[i]+j))
return 0;
}
指针数组->存放指针(地址)的数组。 parr是一个指针数组,有三个元素。每个元素是一个字符指针
十一、结构体
#include <string.h>
struct Stu
{
char name[20]; //
int age;
}s1 = {"zhangsan",20};
struct Stu s2 = { .name="zhangsan",.age=20 };
void set_stu(struct Stu* ps)
{
ps->age = 20;//结构体指针->结构体成员
//ps->name = "zhangsan";//err 数组名是地址,字符串无法放到地址上,而是要放到地址所指向的空间里面去
//t.name=“zhangsan";//err
strcpy(ps->name, "张三");//字符串拷贝 //结构体指针访问变量的成员(->)
}
void print_stu(struct Stu t)
{
printf("%s %d\n", t.name, t.age); //结构体变量访问成员通过点操作符(.)访问
}
int main()
{
struct Stu s = { 0 };
set_stu(&s);
print_stu(s);
return 0;
}
在C语言中,结构体(struct)中的成员不能直接使用字符串字面量进行赋值,特别是当成员是一个字符数组时,原因主要有以下几点:
1.赋值与初始化的区别
在结构体变量声明的同时进行初始化(如 struct Stu s1 = { “zhangsan”, 20 };这个实际上是在声明一个字符数组s1并立即使用字符串字面量"zhangsan"来初始化)是合法的,因为这是在编译时确定的值,并且是整体对象(包括其所有成员)的初始化。但是,一旦结构体变量被声明并初始化后,其成员就不能像变量初始化那样直接赋值了。
2.字符数组的特性
当结构体中的成员是字符数组时,你不能像赋值整数或浮点数那样直接赋值一个字符串字面量。因为字符数组在内存中是一个连续的字符序列,你不能将整个字符串字面量直接“放”到数组中,而是需要将字符串字面量中的每个字符复制到数组中的相应位置。
字符数组:是一个可以在运行时被修改的内存区域。你可以通过索引来读写它的内容。
字符串字面量:通常存储在只读的内存区域(如文本段或常量区)。在大多数情况下,你不能修改字符串字面量的内容。但是,你可以将字符串字面量的内容复制到字符数组中,然后修改字符数组的内容。
3.左值的要求
在C语言中,赋值操作符(=)的左侧必须是一个左值(lvalue)。左值指的是可以位于赋值操作符左侧的表达式,它表示一个存储位置,你不能直接对整个数组进行赋值,因为数组名在大多数上下文中(除了作为&操作符的操作数时)会退化为指向其第一个元素的指针,而不是一个左值。
4.使用strcpy或类似函数
要将一个字符串字面量复制到字符数组中,你需要使用strcpy函数(或类似的函数,如strncpy),这个函数会逐个字符地从源字符串复制到目标字符数组,直到遇到源字符串的终止符\0或达到目标数组的大小限制。
char s[10] = "zhang";
s[10] = "li"; //实际上这样的赋值是错误的,因为左边是访问数组的第11个元素,而右边是一个字符串字面量,类型为char [3]。
//因此,只能通过第一行来进行声明,然后再初始化
//如果已经声明了,就要使用strcpy函数来复制字符串
strcpy(s,"li");
//总结:正确的初始化方式是使用数组名和等号 = 在声明数组的同时进行初始化,或者使用 strcpy 函数在数组已经声明后进行赋值。你不能直接赋值给数组的一个元素(除非那个元素是字符类型,并且你正在赋值一个字符)。
十二、寄存器
eax:通用寄存器,保留临时数据,常用于返回值
ebx:通用寄存器,保留临时数据
ecx:扩展计数器寄存器
edx:扩展数据寄存器
edi:附加目标寄存器
esi:附加源寄存器
ebp:栈底寄存器
esp:栈顶寄存器
eip:指令寄存器,保存当前指令的下一条指令的地址
常见的汇编指令
mov:数据转移指令
push:数据入栈,同时esp栈顶寄存器也要发生改变
pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
sub:减法命令
add:加法命令
call:函数调用,1.压入返回地址 2.转入目标函数
jump:通过修改eip,转入目标函数,进行调用
ret:恢复返回地址,将返回地址pop至eip寄存器