定义符号常量:
格式:#define 标识符(大写) 常量 (小写) 把所有标识符替换成常量
转义字符:
\0表示字符串到此为止 \b表示退格的意思相当于backspace %d整形 %f 浮点型 %p 打印地址类型 %s 字符类型
运算符:
c语言通过提供运算符来支持我们对数据进行处理
表达式:
用运算符和括号将操作数连接起来的式子
sizeod运算符:
sizeof运算符用于获得数据类型或表达式的长度
-sizeof(object); sizeof(对象);
-sizeof(type_name); sizeof(类型);
-sizeof object; sizeof 对象;
signed 和 unsigned :
[signed] int
unsigned int
类型限定符、用于限定char类型或者任何整形变量的取值范围、
signed 带符号位的(可以存放负号数)、unsigned不带符号位(只能存放0和正数)
符号位:
存放signed类型的存储单元中、左边第一位表示符号位。如果该位为0、表示整数为正、如果该位为 1 表示整数为负数。
int result = 2; printf("result = %d\n",result);
unsigned int result = 2; printf("result = %u\n",result);
一个 32 位的整型变量、除去左边第一位符号位、剩下的表示值的只有 31 个比特位。
事实上计算机是用补码的形式来存放整数的值。
补码:
正数的补码是该数的二进制形式。
负数的补码需要通过以下几步获得:
1.先取得该数的绝对值的二进制形式
2.再将第一步的值按位取反
3.最后将第二步的值加1
比特位:CPU 能读懂的最小单位——比特位、bit 、b
字 节: 内存机构的最小寻址单位 —— 字节 、Byte 、B
1Byte == 8bit
1个字节可以表示多大的数 : 11111111 十进制为 255 十六进制为 FF
二进制转换十进制的算法:2 的 n 次方减 1
关系运算符:
优先级相同(高):< <= > >=
优先级相同(底):== != (=是赋值 ==是判断两数是否相等)
逻辑运算符:(返回逻辑值:真1 假0)
! 逻辑非 优先级:高 !a 若 a 为真、!a为假
&& 逻辑与 优先级:中 a&&b 只有 a b 同时为真、结果才为真、否则为假
|| 逻辑或 优先级:低 a||b a b 只要有一个为真、结果为真、否则为假
短路求值:
短路求值又称最小化求值、是一种逻辑运算符的求值策略。只有当第一个操作数的值无法确定逻辑运算的结果时、才对第二个操作数进行求值
C语言对于逻辑与和逻辑或采用短路求值的方式
过滤回车:写程序时、有时在控制台输入后 、敲的回车会被程序接收、此时需要过滤掉 getchar();
#include<stdio.h>
int main()
{
int count = 0; //初始化计数器
printf("请输入英文字符");
while (getchar()!='\n') //循环条件
{
count=count+1; //更新计数器
}
printf("你共输入了%d个字符\n",count);
return 0;
}
for(循环初始化;循环条件;循环调整){
循环体;
}
goto语句:
语法: goto 标签;跳转到标签的位置 在开发过程中应避免使用、会破坏原有代码的逻辑
字符串处理函数:
获取字符串的长度:strlen
拷贝字符串: strcpy 和 strncpy
链接字符串: strcat 和 strncat
比较字符串: strcmp 和 strncmp
避免访问未初始化的指针
数组名其实是数组第一个元素的地址
*(arry+i)==arry[i] *(*(array+1)+3)=array[1][3]
数组指针和二维数组:
定义一个数组指针:int (*p)[3]; p是指向三个元素的数组指针
int (*p)[3]=array;
void指针:
void 是无类型的、但是void指针被称为通用指针、就是可以指向任意类型的数据。即任何类型的指针都可以赋值给void指针
Null指针:
int *p; 定义指针却不给它赋值的话、默认值是随机的、被称为、迷途指针、野指针、悬空指针。程序出错时不好排查
当不清楚要将指针初始化为什么地址时,可以初始化为NULL、在对指针进行解引用时、先检索该指针是否为NULL、为大型程序节约时间
形参和实参:
形参:形式参数 并没有传递确定的值、只是个占位符 只在函数内部有效
实参:实际参数 调用时传递了确定的值 调用时实参会传给形参 单向的
可变参数:
#include <stdarg.h>
-va_list
-va_start
-va_arg
-va_end
#include<stdio.h>
#include<stdarg.h>
int sum(int n, ...); //指定后面有多少个参数 ...作为占位符 表示参数数目不确定
int sum(int n,...)//n为3表示后面有三个参数 求所有参数的和
{
int i, sum = 0;
va_list vap;//定义参数列表 其实是定义一个字符指针的类型
va_start(vap, n);//初始化参数列表 对字符指针进行一个计算
for (i = 0; i < n;i++)
{
sum += va_arg(vap,int);//获取每个参数的值
}
va_end(vap);//关闭参数列表
return sum;
}
int main()
{
int result;
result = sum(3, 1, 2, 3);
printf("result1=%d\n", result);
result = sum(3, 11, 2, 3);
printf("result2=%d\n", result);
result = sum(3, 1, 22, 3);
printf("result3=%d\n", result);
return 0;
}
传值和传址:
每个函数的内部都是互相独立的、它们的变量只在函数内部生效、不同函数之间是不能访问对方的变量的(指针可以解决)
传数组其实是传的地址
指针函数:
函数的类型实际上指的就是函数的返回值
使用指针变量作为函数的返回值、就是指针函数
#include<stdio.h>
char *getword(char);
char *getword(char c)
{
switch (c)
{
case 'A':return "Apple";
case 'B':return "Banana";
case 'C':return "Cat";
case 'D':return "Dog";
default:return "None";
}
}
int main()
{
char input;
printf("请输入一个字母:");
scanf("%c", &input);
printf("%s\n", getword(input));//getword获得的是一个字符串 通常没有类型定义字符串、都是用char类型的指针来定义
return 0; //char类型的指针实际上是指向一个字符、我们用它来指向字符串的第一个字符 截止于空字符\0
}
注意:不要返回局部变量的指针 局部变量:在函数里定义的变量作用范围仅限于函数内部:
#include<stdio.h>
char *getword(char);
char *getword(char c)
{
char str1[]="Apple";
char str2[]="Banana";
char str3[]="Cat";
char str4[]="Dog";
char str5[]="None";
switch (c)
{
case 'A':return str1;
case 'B':return str2;
case 'C':return str3;
case 'D':return str4;
default:return str5;
}
}
int main()
{
char input;
printf("请输入一个字母:");
scanf("%c", &input);
printf("%s\n", getword(input));//getword获得的是一个字符串 通常没有类型定义字符串、都是用char类型的指针来定义
return 0; //char类型的指针实际上是指向一个字符、我们用它来指向字符串的第一个字符 截止于空字符\0
}
程序也能运行但是输入没有输出 警告信息:返回的是局部变量的一个地址
函数指针:
指针函数 ->int *p();
函数指针 ->int (*p)();
#include<stdio.h>
int square(int);
int square(int num)
{
return num * num;
}
int main()
{
int num;
int (*fp)(int);
printf("请输入一个整数:");
scanf("%d", &num);
fp = square;//与int square(int num)函数是等价的、得到此函数的地址
printf("%d*%d=%d\n", num, num, (*fp)(num));
return 0;
}
#include<stdio.h>
int add(int, int);
int sub(int, int);
int calc(int (*fp)(int, int), int, int);
int add(int num1,int num2)
{
return num1 + num2;
}
int sub(int num1,int num2)
{
return num1 - num2;
}
int calc(int (*fp)(int, int), int num1, int num2)//int (*fp)(int, int)是指针、指向一个函数
{
return (*fp)(num1, num2);
}
int main()
{
printf("3+5=%d\n", calc(add, 3, 5));
printf("3-5=%d\n", calc(sub, 3, 5));
return 0;
}
函数指针作为返回值
若一个函数为select、本身有两个参数、返回返回值是一个函数指针、这函数指针也有两个参数、并且其返回值为整型
#include<stdio.h>
int add(int, int);
int sub(int, int);
int calc(int (*)(int, int), int, int);
int (*select(char))(int, int);//select后面是一个参数列表、可以确定select是一个函数 参数是char类型,返回的是一个指针、返回整型并且带有两个参数的函数指针
int add(int num1,int num2)
{
return num1 + num2;
}
int sub(int num1,int num2)
{
return num1 - num2;
}
int calc(int(*fp)(int,int),int num1,int num2)
{
return (*fp)(num1, num2);
}
int (*select(char op))(int,int)
{
switch (op)
{
case '+':return add;
case '-':return sub;
}
}
int main()
{
int num1, num2;
char op;
int (*fp)(int, int);
printf("请输入一个式子(如1+3):");
scanf("%d%c%d", &num1, &op, &num2);
fp = select(op);
printf("%d%c%d=%d\n", num1, op, num2, calc(fp, num1, num2));
}
extern关键字 一个函数的全局变量在函数定义之后定义
用extern关键字告诉编译器、这个变量我在后面定义了、你先别报错
不要大量使用全局变量
使用全局变量会使你的程序占用更多的内存,因为全局变量从被定义时候开始,直到程序退出才被释放。
污染命名空间,虽然局部变量会屏蔽全局变量,但这样一来也会降低程序的可读性,人们往往很难一下子判断出每个变量的含义和作用范围。
提高了程序的耦合性,牵一发而动全身,时间久了,代码长了,你都不知道全局变量被哪些函数修改过。
作用域
变量被定义在程序的不同位置时、其作用范围不一样。这个作用范围就是作用域。
C语言编译器可以确认4种不同类型的作用域
——代码块作用域 :
(block scope) 在代码块中定义的量、具有代码块作用域。作用范围从变量定义开始、到标志该代码块结束的右大括号(})结束。
尽管函数的形式参数不在大括号内定义、但其同样具有代码块作用域、隶属于包含函数体的代码块。
——文件作用域 :
(file scope) 任何在代码块之外声明的标识符都具有文件作用域、作用范围从它们的声明位置开始、到文件的结尾处都是可以访问的。
另外、函数名也具有文件作用域、因为函数名本身也是在代码块之外。
——原型作用域 :
(prototype scope) 原型作用域只适用于那些在函数原型中声明的参数名。函数在声明的时候可以不写参数的名字(但参数类型是必须要写上的)、其实函数原型的参数名还可以随便写一个名字、不必与形式参数相匹配(这样无意义)
——函数作用域 :
(function scope) 函数作用域只适用于goto语句的标签、作用将goto语句的标签限制在同一个函数内部、以防止出现重名标签。
定义和声明
定义:当一个变量被定义的时候、编译器为变量申请内存空间并填充一些值。
声明:当一个变量被声明的时候、编译器就知道该变量被定义在其他地方。
声明是通知编译器该变量名及相关的类型已存在、不需要再为此申请内存空间。
局部变量既是定义又是声明。定义只能来一次、否则就叫做重复定义某个同名变量、而声明可以有很多次。
链接属性
编译器把源文件变成可执行文件需要两个步骤:编译、链接
编译:将源代码生成机器代码也就是目标文件
链接:将目标了文件及相关的库文件合并得到可执行文件
链接属性三种:
external(外部的):多个文件声明的同名标识符表示同一个实体
internal(内部的):单个文件中声明的同名标识符表示同一个实体
none(无):声明的同名标识符被当作独立不同的实体
只有具备文件作用域的标识符才能拥有external或internal的链接属性、其他作用域的标识符都是none属性
默认情况下、具备文件作用域的标识符拥有external属性。也就是说该标识符允许跨文件访问。对于external属性的标识符、无论在不同文价中声明多少次、表示的都是同一个实体。
使用static关键字可以使原先拥有external属性的标识符变为internal属性。这里有两点需要注意:
1、只对具有文件作用域的标识符生效(对于拥有其他作用域的标识符是另一种功能)
2、链接属性只能修改一次、也就是说一旦将标识符的链接属性变为internal、就无法变回external了
生存期
C语言的变量拥有两种生存期
1、静态存储期
2、自动存储期
具有文件作用域的变量隶属于静态存储期、函数也属于静态存储期。属于静态存储期的变量在程序执行期间将一直占据存储空间、直到程序关闭才释放。
具有代码块作用域的变量一般情况下属于自动存储期。属于自动存储期的变量在代码块结束时将自动释放存储空间。
存储类型
C语言提供了5种不同的存储类型
——auto :(自动变量 ) 在代码块中声明的变量默认的存储类型类型就是自动变量、使用关键字auto来描述
——register:(寄存器变量)将一个变量声明为寄存器变量、那么该变量就有可能被存放于CPU的寄存器中。为啥是有可能:因为CPU内存有限、不可能把所有声明的变量都放进去
当将变量声明为寄存器变量、就不能通过取地址运算符获得该变量的地址
——static :使用static来声明局部变量、那么就可以将局部变量指定为静态局部变量。
static使得局部变量具有静态存储期、所以它的生存期与全局变量一样、直到程序结束后才释放
——extern :告诉编译器这个变量在别的地方定义过了,不要急着报错
——typedef :
递归(更难阅读和维护)
递归必须有结束条件、否则程序将崩溃
实现递归要满足两个条件:1、调用函数本身、2、设置正确的结束条件
慎用递归
#include<stdio.h>
long fact(int num);
long fact(int num)
{
long result;
if(num>0)
{
result = num * fact(num - 1);
}
else
{
result = 1;
}
return result;
}
int main(void){
int num;
printf("请输入一个数:");
scanf("%d", &num);
printf("%d的阶乘是:%d\n", num, fact(num));
return 0;
}
递归解决汉诺塔问题
#include<stdio.h>
void hanoi(int n, char x, char y, char z);
void hanoi(int n, char x, char y, char z)
{
if(n==1)
{
printf("%c -->%c\n", x, z);//n=1时直接从x移动到z就行了
}
else
{
hanoi(n - 1, x, z, y);//将n-1个圆盘从x借助z移动到y
printf("%c -->%c\n", x, z);
hanoi(n - 1, y, x, z);//把y上的借助x移动到z
}
}
int main(void){
int n;
printf("请输入汉诺塔的层数:");
scanf("%d", &n);
hanoi(n, 'x', 'y', 'z');
return 0;
}
分治法
大事化小、小事化了
快速排序
基本思想:通过一趟排序将待排序数据分割成独立的两部分、其中一部分的所有元素均比另一部分的元素小、然后分别对这两部分继续进行排序、重复上述步骤直到排序完成
#include<stdio.h>
void quick_sort(int array[],int left,int right)
{
int i = left, j = right;
int temp;
int pivot;
pivot = array[(left + right )/ 2];
while (i<=j)
{
//从左到右找到大于等于基准点的元素
while (array[i]<pivot)
{
i++;
}
//从右到左找到小于等于基准点的元素
while (array[j]>pivot)
{
j--;
}
//如果i<=j、则互换
if(i<=j)
{
temp = array[i];
array[i] = array[j];
array[j] = temp;
i++;
j--;
}
}
if(left<j)
{
quick_sort(array, left, j);
}
if(i<right)
{
quick_sort(array, i,right);
}
}
int main(void)
{
int array[] = {73,108,111,118,101,70,105,115,104,67,46,99,111,109};
int i, length;
length = sizeof(array) / sizeof(array[0]);
quick_sort(array, 0, length - 1);
printf("排序后的结果是:");
for (i = 0; i < length;i++)
{
printf("%d ", array[i]);
}
putchar('\n');
}
更灵活的内存管理方式
----malloc
————申请动态内存空间
函数原型:void *malloc(size_t size);
malloc函数像系统申请分配size个字节的内存空间(并没有初始化为0、是随机的)、并返回一个指向这块空间的指针(void类型:因为void可以转换为任意其他类型)。如果函数调用成功、返回指向申请的内存空间的指针、若调用失败、返回值是NULL。如果size参数设置为0、返回值也可能是NULL、但并不意味着函数调用失败。
----free
————释放动态内存空间
函数原型:void free(void *ptr);
free函数释放ptr参数指向的内存空间。该内存空间必须是由malloc\calloc\realloc函数申请的。否则、该函数将导致未定义行为。若ptr参数是NULL、则不执行任何操作。注意:该函数并不会修改ptr的参数的值、所以调用后它仍然指向原来的地方(变为非法空间)。
----calloc
————申请并初始化一系列内存空间
----realloc
————重新分配内存空间
内存泄漏的原因
————隐式内存泄漏(用完的内存块没有及时用free函数释放)
————丢失内存块地址
malloc还可以申请一块任意尺寸的内存空间
#include<stdio.h>
#include<stdlib.h>
int main(void)
{
int *ptr = NULL;
int num, i;
printf("请输入待录入数的个数:");
scanf("%d", &num);
ptr = (int *)malloc(num * sizeof(int));
for (i = 0; i < num;i++)
{
printf("请录入第%d个整数:", i + 1);
scanf("%d", &ptr[i]);
}
printf("你录入的整数是:");
for (i = 0; i < num;i++)
{
printf("%d", ptr[i]);
}
return 0;
free(ptr);
}
初始化内存空间
以mem开头的函数被编入字符串标准库、函数的声明包含在String.h这个头文件中
memset————使用一个常量字节填充内存空间
memcpy————拷贝内存空间
memmove————拷贝内存空间
memcmp————比较内存空间
memchr————在内存空间中搜索一个字符
calloc
函数原型:- void *calloc(size_t nmemb, size_t size);
calloc函数在内存中动态地申请nmemb个长度为size的连续内存空间(即申请的总空间尺寸为nmemb*size),这些内存空间全部被初始化为0。
malloc与calloc的区别
calloc函数在申请完内存后,自动初始化该内存空间为零
malloc函数不进行初始化操作,里边数据是随机的
realloc
函数原型——void *realloc(void *ptr,size_t size);
以下几点是需要注意的:
- realloc函数修改ptr指向的内存空间大小为size字节
一如果新分配的内存空间比原来的大,则旧内存块的数据不会发生改变;如果新的内存空间大小小于旧的内存空间,可能会导致数据丢失,慎用!
一该函数将移动内存空间的数据并返回新的指针
一如果ptr参数为NULL,那么调用该函数就相当于调用malloc(size)
一如果size参数为日,并且ptr参数不为NULL,那么调用该函数就相当于调用free(ptr)
一除非ptr参数为NULL,否则ptr的值必须由先前调用malloc、 calloc或realloc函数返回
C语言的内存布局规律
堆
堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩展或缩小。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上;当利用free等函数释放内存时,被释放的内存从堆中被剔除。
栈
大家平时可能经常听到堆栈这个词,一般指的就是这个栈。栈是函数执行的内存区域,通常和堆共享同一片区域。
堆和栈的区别
申请方式
------堆由程序员手动申请
------栈由系统自动分配
释放方式
------堆由程序员手动释放
------栈由系统自动释放
生存周期
------堆的生存周期由动态申请到程序员主动释放为止、不同函数之间均可自由访问
------栈的生存周期由函数调用开始到函数返回时结束、不同函数之间的局部变量不能互相访问
发展方向
------堆和其它区段一样、都是从低地址向高地址发展
------栈相反、由高地址向低地址发展