C基础学习
1. 正数的补码是它本身、负数的补码是它的绝对值取反、+1
2. ASCII码为0 的字符是 NULL
3. char型和整型一样、都是以补码的形式存储、它其实可以是 -128 ~ 127 所表示的范围、unsigned char是0~255
4. float其实不是精确的数值
5. 不同形式的0值 0、'\0'、'0'、"0" 分别是常量0、空字符NULL、字符0、字符串0、
6. 常量:整型常量、实型常量、字符常量、字符串常量、标识常量define
7. gcc预处理:-E、
常量
#define PI 3.14
#define ADD (2+3)
#define MAX(a, b) a > b ? a : b
#define的内容会在预处理的时候解析处理
变量
用来保存特定内容、在程序执行过程中值随时会发生变化的量
[存储类型] 数据类型 标识符 = 值
type name = value
数据类型:基本数据类型 + 构造类型
存储类型:auto static register extern(说明型)
默认auto:自动分配空间、自动回收空间
register:寄存器类型、只能用来定义局部变量、不能定义全局变量、只能定义32位大小的数据类型
默认auto、放在栈空间、
register存放在CPU、存取速度快
static:静态型、自动初始化为0值或者空值、并且其变量的值有继承性、
修饰变量或者函数时、也把变量或者函数的作用域限制在本文件内
extern:说明型、说明有一个定义在外部、意味着不能改变被说明的变量的值或者类型
auto类型若不分配初始值、值的大小是不确定的、static一定会被初始化为0值
变量的生命周期和作用范围
1)全局变量的作用范围:从变量定义开始、到程序执行结束
局部变量的作用范围:从局部变量定义开始、到语句块执行结束
定义全局变量其实就是多个模块会公用某个变量、一个模块对变量的修改为影响其它模块的使用
__FUNCTION__ 当前所在函数、gcc提供
2)static 作用域、定义开始到本文件执行结束
运算符和表达式
1. sizeof 其实不是关键字、而是运算符、用来测试字符或者数据类型的长度
2. 强制类型转换、转换的是中间过程、而不是变量本身
3. 位运算应用
1)将操作数的某一位置1、其它位不变 num | 1 << n
2)将操作数第n为清0 num & ~(1 << n)
3)测试第n位 num & 1 << n
4) 从一个指定宽度的数中取出其中的某几位 ??
输入/输出
分为标准IO 和 系统调用IO
1. 格式化输入输出函数 scanf、printf
2. 字符串输入输出函数:getchar、putchar
3. 字符串输入输出函数:gets(!)、puts()
格式控制修饰符
m数据宽度 若int a = 123 、printf("%4d") 会输出空格+123 printf("%2d") 会输出123、也就是宽度不够时、忽略、宽度过宽时、补空格
n小数点后的位数、四舍五入
两个名字相同的函数、语言未知、如何判定是重载实现的还是变参实现的 ?
多传几个参数进去、如果在编译时报语法错误、则是重载实现的
如果编译时不报错、在使用时发现不符合预期、则是用变参实现的
exec族是变参实现的
缓冲机制:
当缓冲机制为行缓冲时、\n强制刷新缓冲区
数组名、本身就是地址常量
scanf必须按照format原样输入
1. scanf使用%s输入字符串时、无法检测输入字符的长度是否已越界
2. 放在循环里、必须校验scanf的返回值(如果要求输入%d(int)、实际输入%c(char)、程序会一直输出上次放到缓冲区的内容)
回车的ascii的码值是10
scanf和getchar连用时、回车本来是想结束scanf的输入、缺被getchar接收、可以使用抑制符*吃掉一个字符、或者用getchar吃掉
scanf("%d", &i);
getchar();
scanf("%c", &ch)
gets(danger): 不检测输入是否越界
fgets保证不出错、单不能保证接收你需要输入的全部、可能想输入10个、只被接收了5个
getline可以接收一个完整的串、也不会越界、因为使用的是动态内存来实现的
所以gets可以使用fgets或者getline来代替、注意getline不是c88或者c99标准库、值存在gunlib里
CFLAGS += -lm 在进行gcc编译时、添加-lm选项
C中进行混合数据类型的判断时、要让数据精度一致
数组
1. 一维数组
1) 定义:[存储类型] 数据类型 标识符 [下标]
2)初始化:
可以不初始化(值不确定、是它指向的内存空间的值)
全部初始化(用大括号包括)
部分初始化(未被初始化的值会被初始化为0)
使用static定义 就算全部没有初始化也会被全部被初始化化为0
3)元素引用
4)数组名 是表示地址的常量、也是数组的起始位置
5)数组越界
应用:
1)冒泡排序 2)选择排序 3)删除发求质数 4)进制转换
2. 二维数组
应用:1)行列互换 2)最大值及其下标 3)求二维数组中各行和各列的和 4)
3. 字符数组
可以使用单个字符或者字符串常量来初始化
会有一个结束标记\0
先放到进程环境的输入缓冲区、然后再写入指定空间
strlen是以\0为标记、以后的字符不计算 sizeof是实际占用的大小、包含\0
指针
1. 变量与地址
变量名其实是给用户用的、地址是给计算机用的、计算机凭借地址值找到某个空间
变量名就是抽象出来的某块地址的名称
2. 指针与指针变量
指针变量:保存指针的变量
指针:具有指向作用的地址
3. 直接访问与间接访问
指针变量所占的地址空间大小是确定的、64位平台是占8个字节、32位是占4个字节
但是不同类型的指针在运算时、意义不同、int *i= &x 会到i所指向的空间取4个字节、double *会取8个字节 char *会取1个字节
int *p = &i
type name = value
type是int * name是p、意义是 定义一个指针类型的变量p、将i的地址赋值给p
4. 空指针与野指针
NULL是define的一个宏、值为0、*p = NULL 也就是将指针指向起始地址为0的一个空间,它不分配给任何人使用、在不确定将指针指向何处时、可以先赋为空指针
野指针是所指向的空间是不确定的、或者压根就没有指向
为了防止野指针的产生、可以在定义指针变量时、给初值NULL int *i = NULL; 在有指向时再赋值即可
void * 可以接收任何类型的指针、也可以赋值给任意类型、在不确定要操作的值类型时可以使用void *
viod *p = NULL
5. 空类型
6. 定于与初始化的书写规则
7. 指针与运算
& * 关系运算 ++ --
8. 指针与数组
int a[3] = {1, 2, 3}
int *p = a; p指向a的起始地址、与a的区别是:p是变量、a是常量
int a[2][3] = {{1,2,3}, {4,5,6}} a是在行上移动的指针 *(a+i)变成列指针
int *p p是在列上移动的指针
char str[] = "hello";
// str = "world"; // 错误的写法、数组名是地址常量
strcpy(str, "wrld"); // 新的字符串比原空间大时、会产生core dump·
puts(str);
/*
char *str = "hello";
strcpy(str, "world"); // 是把原空间的常量覆盖、不允许
str = "world"; // 把str这个指针指向world这个常量即可
puts(str);
9. const与指针
const把某些内容常量化
float pi = 3.14 用变量来保存常量值、但它的值可能被外部改变
const float pi=3.14 把变量pi常量化、不允许修改
与define比会进行语法检测
const int *p; 是指针常量
int const *p; 等价
常量指针:(const int *p)
指针的指向可以发生变化、但指针所指向的当前空间的值不能发生变化
指针常量:(int *const p)
指针的指向不能发生变化、但指针所指向的目标变量的值可以发生变化
const int *const p = &i; //
常量指针举例:
int i = 1;
const int *p = &i;
i = 10; // 可以改变变量的值
*p = 10; // 不可以、因为*p被const修饰、常量化了
const修饰值不能变化、只是锁定通过这个名字不能发生变化、而不是目标变量的值不能变化、可以通过其它方式改变
int j = 100;
p = &j; // 可以、把p指向j的地址
指针常量举例:
int i = 1;
int *const p = &i;
*p = 10; // 可以、改变p指向地址的值
p = &j; // 不允许、指针常量不允许修改指针的指向
10. 指针数组和数组指针
数组指针:是指向数组的指针、本质是一个指针
[存储类型] 数据类型 (*指针名)[下标] = 值
auto int (*p)[3]; ==== int[3] * p
指向包含3个整型元素的数组的指针
指针数组:
[存储类型] 数据类型 * 数组名 [长度]
int * arr[3]; -> type name === int *[3] arr
包含3个指针变量的数组
char * name[5] = {"follow me", "basic", "great", "ccc"}
定义一个数组name、包含5个元素、每个元素是一个指向char 类型的指针
11. 字符指针和字符数组
函数
1. 函数的定义
数据类型 函数名 ([形参说明表]) -- 数据类型 形参名, 数据类型 形参名, ...(可能是定长参数、也可能是变长参数)
2. 函数的传参
1)值传递
2)地址传递 需要在另外的函数中改变其它函数的值的时候、可以使用地址传参
3)全局变量
3. 函数的调用
return 0; // 结束当前函数
echo $?; 输出上次命令的返回值
一个进程的返回状态、是给它的父进程看的
int main(int argc, char *argv[])
int main(int argc, char **argv) --> **argc 其实就是 *argv[]就是一个argv的数组起始地址
argc 是计数器、计算从终端传入的参数个数
argv 是列表、用来保存从终端传入的所有参数(其实就是字符指针数组的首地址)
每个char *指向一个参数字符串、最后以空指针NULL结束
当前shell可以解析通配符
1. 嵌套调用
2. 递归调用
何时跳出、检错、
递归会有调用栈、递归层级太深的时候、会造成栈溢出(栈空间大小是一定的)
传数组名的时候、其实是把数组第一个元素的地址传入
void print_arr(int p[], int n)
void print_arr(int *p, int n)
在作为形参时、int *P 等价于 p[]
在定义变量时、表示分配多大内存的空间 int a[] = {1, 2, 3}
int a[N] = {1, 2, 3}
在使用以下值作为实参时、对应的形参应该是?
-> a *a a[0] &a[3] p[i] p *p p+1
-> int * int int int * int int * int int *
4. 函数与数组
二维数组
int a[M][N] = {1, 2, 3, 4, 5, 6}
1. 作为一个大的一维数组传入
print_arr(&a[0][0], M*N) // 或者 *a, M*N | a[0]--> *(a+0)
print_arr(*p, int n)
// 这样无法区分行、列
2. 作为二维数组传入、区分行、列
print_arr(a, M, N)
print_arr(int (*p)[N], int m, int n) // 数组的本质是 *(a+i)[j] 行列指针(一个指向数组的指针)
printf("%4d ", *(*(p+i)+j));
p其实就是一个一级指针、指向一个数组的起始位置
或者写成
print_arr(int p[][N], int m, int n)
printf("%4d ", p[i][j]);
3. int a[M][N] = {...};
int *p = *a; --> p相当于一个行指针、所以p[i] 相当于某个元素
int (*q)[N] = a; --> q[i][j] ==> a[i][j]
在使用以下值作为实参时、对应的形参应该是?
-> a[i][j] *(a+i)+j a[i]+j p[i] *p q[i][j] *q q p+3 q+2
-> int int * int * int int int int * int (*)[N] int * int (*)[N]
5. 函数与指针
指针函数(之前的int func(arg) 可以称为整型函数)
返回值 * 函数名(形参)
eg int * fun(int)
函数指针
类型 (*指针名) (形参表)
eg. int (*p)(int)
指的是一个指针、它指向一个函数
int (int, int) *p ==> int *p(int, int)
函数指针数组
类型 (*数组名[下标]) (形参)
eg. int (*arr[N])(int);
数组arr包含N个元素、这n个元素都是指向函数的指针
int (int, int) *funcp[2] ==> int (*funcp[2])(int, int)
指向指针函数的函数指针数组
int *(*funcp[M])(int)
构造类型
1. 结构体
产生及意义:描述不同数据类型的结构
类型描述
struct 结构体名{
数据类型 成员1;
数据类型 成员2;
...
}; // 分号必须有
结构体的类型描述不占用任何空间、不能直接使用等号初始化
int i; // int 是描述、不占用空间 i是变量、占用空间、struct一样、描述不占用空间、定义变量时才占用
一般定义在函数体外
嵌套定义
定义变量(变量、数组、指针)、初始化及成员引用
成员引用:变量名.成员名
struct stu stu = {.math = 98, .chinese = 97} // 只给结构体中部分元素赋值
或者 指针 -> 成员名
struct stu *p; // 定义一个指向结构体的指针
(*指针).成员名
结构体数组
struct st arr[2] = {{"zhang", 11, 98}, {"li", 12, 99}};
注意:在给name赋值时、因为str[NAMESIZE]数组名的数组名str只是数组的起始位置、 不能直接使用 stu.str = "name"、可以使用 strcpy(stu.name, "name")
struct simp_t
{
int i;
char ch;
float f; // 占用4个字节
char ch1; //
} __attribute__((packed)); // 告诉结构体不要进行内存对齐、在网络传输时常用
函数传参(值、地址)
2. 共用体
产生及意义:解决硬件空间不足、同一时刻、n个成员间只能有一个在使用
类型描述
union 共用体名
{
数据类型 成员名1;
数据类型 成员名2;
...
};
嵌套定义
定义变量(变量、数组、指针)
初始化成员及引用
变量名.成员名
6位 指针名->成员名
函数传参(值、地址)
位域
得到32位数字的值
unsigned int32_t i = 0x11223344;
i>>16(高1) + i & 0xFFFF(低16位)
union{
struct{
uint16_t i;
uint16_t j;
}x;
uint32_t y;
}a;
给y赋值、可以得到x+y
3. 枚举值
enum 枚举名称
{
成员1 ;
成员2 ;
...
};
可以当成宏值来使用
enum
{
STATE_RUNNING = 1;
STATE_RUNNING ;
STATE_OVER;
};
直接使用宏定义、在预编译时会被替换成常量值
使用enum是结构体定义、不会在预处理是被替换掉
动态内存管理
1. auto修饰的变量在栈上 static修饰在静态区上, 根据全局还是局部、是否初始化再细化
2. 动态内存可以方便的根据需要来申请空间
原则:谁申请谁来释放
3. void *malloc(size_t size);
realloc 从给定地址处查看是否具有申请的size字节、没有的话、从别处重新查找申请、返回新的空间地址、释放原空间地址、
若是缩小、直接在原基础上从尾压缩
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *p;
p = malloc(sizeof(int));
直接用p接收即可、若不包含头文件stdlib、编译器会看不到malloc的函数原型、默认>为int *、就会想要类型强转
if (p == NULL){
printf("malloc error.");
exit(1);
}
*p = 10;
printf("%d\n", *p);
free(p);
exit(0);
}
free之后空间还在、但是可能会重新分配
free之后、要有把指针赋值为空的习惯
3. typedef 针对已有类型去设置新的数据类型(修改原有数据类型名)
typedef 已有的数据类型 新名字;
typedef int FUNC(int) 将参数为int、返回值为int的函数改名为FUNC -> int(int) FUNC
eg. FUNC f; --> int f(int);
typedef int *FUNCP(int);
eg. FUNCP f; --> int *f(int) 给返回值为int *、参数为int的函数FUNCP改名
typedef int *(*FUNCP)(int);
eg. FUNCP f; --> int *(*f)(int) 定义指针变量p、它指向返回值为int *、参数为int的函数
是指向指针函数的指针变量