C语言学习笔记

本文为本人本科时学习C语言时的笔记。

1.C语言入门

C语言是一门面向过程的计算机编程语言

冯诺依曼体系结构:计算机硬件由运算器、控制器、存储器、输入设备和输出设备五大部分组成。
运算器+控制器=CPU
冯·诺依曼是当之无愧的数字计算机之父。

//VS注释代码:ctrl k c
//反注释:ctrl k u

main函数是一个c语言的主函数(入口函数),
return语句用于函数的返回,return表示函数执行完了。
stdio.h等头文件是vs安装时自带的,包含大佬们写好的现成的代码。
<> 优先查找系统目录 ““优先查找当前项目的目录。
printf是stdio.h包含的一个库函数。
%d 叫做格式化字符串,%d意味着即将打印一个十进制整数。 %S格式对应的是字符串
scanf(”%d %d”, &coding),输入必须和括号里格式一样,逗号就是逗号,空格就是空格。建议使用空格,逗号分中英文,用户会迷糊.
字符需要加上单引号’_’
int a=0;定义变量要先初始化

逻辑:
&& 逻辑与,都真为真
|| 逻辑或,一真即真
! 逻辑反
^ 异或,相同为0,不同为1

数据类型:
K=thousand=一千,M=million=百万, G=billion=十亿,PB=1024TB
//CPU主频2.4GHz就是指一秒钟执行24亿条指令
//KB=1024Byte 字节,1Byte=8二进制位–>1字节=8位,int=4字节=32位bit
char 1字节,范围是0-255,2^8=256. -128—127
short 2字节,范围是0-65535,2^16=65536. -32768—32767
int,4字节,2^32=42亿9千万 -21亿----21亿 整形优先用!int不变,指针大小才分4/8字节
double,8字节,浮点型优先用!
long 4字节
long long 8字节
float 4字节

作用域/生命周期:
局部变量的作用域是变量所在大括号。
全局变量的作用域是整个工程。
局部变量的生命周期是:进入作用域生命周期开始,出作用域生命周期结束。
全局变量的生命周期是:整个程序的生命周期。

对于局域变量,没有初始化,值为随机值(实际为栈上残留数据).
对于全局变量,没显式初始化,就会默认初始化成0.

static:
修饰局部变量改变了变量的生命周期,让静态局部变量出了作用域依然存在,到程序结束,生命周期才结束。//生命周期+1s
一个全局变量被static修饰,使得这个全局变量只能在本源文件(.c)内使用,不能在其他源文件内使用。//生命周期-1s
一个函数被static修饰,使得这个函数只能在本源文件内使用,不能在其他源文件内使用。//生命周期-1s

转义字符:
’ 用于表示字符常量’
\“ 用于表示一个字符串内部的双引号
\ 用于表示一个反斜杠,防止它被解释为一个转义序列符

内存:
是电脑上特别重要的存储器,计算机中所有程序的运行都是在内存中进行的。
所以为了有效的使用内存,就把内存划分成一个个小的内存单元,每个内存单元的大小是1个字节.
为了能够有效的访问到内存的每个单元,就给内存单元进行了编号,这些编号被称为该内存单元的地址。

typedef: 类型重命名

结构体: 用户自定制的类型,根据需要定义更有实际意义的变量类型

2.分支语句与循环语句

0表示假 非0表示真

else和最近的if配对

swich是多分支语句,坑太多.break,default,swich(只能是整数)。
if else更好用。尽量不使用swich,看得懂就行。

break 功能–>永久终止循环。
continue功能是跳出本次循环,进入下一次循环。

EOF==end of file
在键盘上按下ctrl+z,就相当于输入了一个EOF字符。只限windows系统。
‘a’ ,不要记asiic码表所表示的数字,用单引号表示最好。

getchar 读一个字符
putchar 输出一个字符

如果已经定义过i(初始化过),可以写成for(;i<10;),i++也可以写在后面
还可以for(int i=0;i<10;i++),c语言中不可以,c++可以,但是vs支持,好处是i作用域只在这个大括号里,不再是全局变量,出错率低。
可以(x=0,y=0;…),在一个表达式里对多个循环变量进行控制。

建议一:不要在循环体中修改循环变量。
建议二:for语句里的循环控制变量使用前闭后开区间
前闭后开 for(int i=0;i<10;i++)—>[0,10)

for(;;++i){
    --i;
}
//会死循环,结果不可预期,最好都放在表达式里

字符串比较要用strcmp,不能用==

rand()函数,取一个0-32767的随机值

//那么这个随机值模n的结果就是0到(n-1), 模n之后+1就是取一个1到n内的随机数,只要调整加号前后两个数字就能随意调整想要的随机区间.
随机种子:随机数的初始值.

时间戳: 以1970年1月1日0时0分0秒为基准 计算当前时间和基准之间的秒数差. int能表示的范围是21亿,20年后就不够用了,尽量不用int使用时间戳.

goto语句可读性差,不建议用.
goto可以一次跳出多层循环,和again配套使用. break是无法实现的.

3.函数

C语言中函数分为:1.库函数 2.自定义函数
库函数就是大佬们写好的,有标准库和第三方库
C语言的标准库很弱,我们工作后需要依赖大量第三方库,而且用起来需要搭建好几个小时环境,其他语言第三方库又多又便捷。
//自定义函数可以首字母大写来方便自我区分,不强制
//想查库函数的详细用法,去cpluscplus官网
//最上方可以看到每一个函数需要包含的头文件,下方有示例,示例旁有RUN可以在线运行。

以\0结尾的字符数组,叫做C风格字符串,即C string,c++里是std::string

函数组成:

ret_type fun_name(para1, * ) 
{ 
    statement;//语句项
} 
ret_type 返回类型 
fun_name 函数名
para1    函数参数

三目运算符

对于条件表达式b ? x : y,先计算条件b,然后进行判断。如果b的值为true,计算x的值,运算结果为x的值;否则,计算y的值,运算结果为y的值。
—>b真,结果为x。b假,结果为y。所以b是条件,后面是两个结果。
在C语言中,两个结果的类型必须一致。

形式参数(形参):

形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。
实际参数(实参):

真实传给函数的参数,叫实参。实参可以是:常量、变量、表达式、函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
函数定义的时候()中写的参数是形参。一开始定义swap时候swap(int x,int y)里的x,y
函数调用的时候()中写的参数是是实参。就是后面main里swap(a,b)的a,b
函数调用过程中,形参是实参的副本

函数声明:

  1. 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,无关紧要。
  2. 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
  3. 函数的声明一般要放在头文件中的。
    函数定义:

函数的定义是指函数的具体实现,交待函数的功能实现。
.h文件放声明, .c文件放定义。先声明后使用

sizeof(arr)/sizeof(arr[0]) 求数组元素个数

隐式类型转换?
void*存储任何类型变量的地址?不能解引用?不能算数运算?泛型编程?
c和c++区别?
《effective c++》必读,开篇就是解答区别

C语言中凡是#开头的都是预处理指令,任何头文件里都包含#pragma once,表示无论被include(包含)几次,实际只复制粘贴一次。

递归??

循环和递归有时可以相互转换,有时前者简单,有时相反,多数情况下循环更高效(函数调用时有成本的)

4.数组

数组创建, [] 中要给一个常量才可以,不能使用变量比如n,n=10。const类型变量算常量,但是c语言不认可会报错,是设计缺陷即bug。文件改为.cpp就可以编译通过。

常量:const修饰/宏定义/枚举(enum)

%x 是以位无符号十六进制形式输出整数

初始化:
int arr[10]={1,2,3};//那么剩余元素填充0.
int arr[10];//那么全部是相同随机值
int arr[]={1,2};[]里不写数字的话数组长度取决于后面写了几个元素。

区别:
char arr1[]="abcd";
char arr2[]={'a','b','c','d'};
字符串后面隐藏了一个/0,所以数组长度为5,下面的是4.

越界:
有可能越界的内存地址里存的是别的变量的内容。

数组在内存中是连续存放的。

二维数组:
int arr[3][4];//长度为3的一维数组,每个元素是一个长度为4的一维数组。
即有编号0,1,2三个元素,每个元素有4个一维数组
3行4列
[][4]行数可以省略,列数不可以省略,c语言的反人性规定
初始化的时候按顺序设置,剩余的为0.

行 Row 列 Column

int arr[3][4]={
    {1,2},{3,4}
}

这样下标为0的元素数组为1 2 0 0
1数组是3 4 0 0
2数组是0 0 0 0

逗号表达式,(1,2),(3,4) 输出的是,2和,4
数组0元素变成2 4 0 0

数组作为函数的参数时,会隐式转换成指针,指向数组首元素。

对于局域变量,没有初始化,值为随机值(实际为栈上残留数据)。
对于全局变量,没显式初始化,就会默认初始化成0.

编辑宏可以全大写,方便理解
全局变量可以加g_前缀(Global variables) 局部变量(Local variables)

前端:html,css,javascript.
公司给钱一样多可以转方向。

5.操作符

srand((unsigned int)time(0));//设置随机种子,time(0)获取当前时间戳.
time函数头文件是#include<time.h>

% 两数必须都是整数

<< 左移操作符 >> 右移操作符,左移1位实际就是把数乘2

int a = 1;
a = a << 2;
//a = 4
//0001->0100
不要移动负数位,这个是标准未定义的,>>-1是不行的,0是可以的

&&两条件都为真则结果为真,||两条件一个为真则结果为真。
&&和||还具有短路的功能
对于&&,第一个表达式为0,结束计算。
对于||,第一个表达式非0,结束计算。

int a = 0x3;
int a = 0x2;
//a & b结果为2,因为
 11
&10
=102

或|也是如此计算,全0则0,否则1
^异或,相同为0,否则为1
取反,0变1,1变0

int num = 1;
  printf("%d\n", ++num);//2
  printf("%d\n",num++);//2
  printf("%d\n", num);//3
int i = 0,a = 0,b = 2,c = 3,d = 4;
i = a++ && ++b && d++;
printf("a = %d\n b = %d\n c = %d\n d = %d\n", a, b, c, d);

程序输出的结果是什么?
a=1,b=2,c=3,d=4
i=这道式子里a++为0,后面与的都是0,根本不计算了.
叫做短路求值.

//i = a++||++b||d++;
1 3 3 4
++b为真后面也不用再计算了,也短路了

整形提升:凡是针对char/short进行数学运算,此时都会先进行隐式转换成int,再运算

算数转换,两不同类型进行计算,由低的向高的转换后进行计算

6.指针

指针就是变量,存放内存单元的地址.

int a = 10;
int* p = &a;
//int*是指针类型,和int char等并列
//取地址符&只能取变量的地址
//&p 解引用,获取房间号所指房间里面的内容

使用指针变量时,必须保证指针中容纳的房间号是程序员申请过的
不能随便指定一个房间号,一旦访问了非法内存(即不是程序员申请过的),未定义行为.
非法地址的代表:空指针,0号地址,int* = NULL
NULL实际就是:#define NULL 0

int* 4字节 一字节是8个二进制位 就是2^32
2^10 * 2^10 * 2^10 *2^2=4GB

32系统和64系统上的程序互相兼容
vs上方选择x64后一个指针大小就是8

int* p = NULL;
printf("%d\n",sizeof(p));

int a = 10;
int* p = &a;
printf("%d\n",*p); 

//结果为10

数组名表示的是数组首元素的地址

int arr[] = {1,2,3,4};
int* p = arr + 1;
printf("%d\n",*p);

结果为2,数组隐式转换成指针后+1就是前进一个int类型大小,就是2的位置

int arr[] = {1,2,3,4};
printf("%d\n",sizeof(arr));
printf("%d\n",sizeof(arr+1));

结果为16和4
同上,数组名进行运算就先变为指针然后运算

int arr[] = {1,2,3,4};
int* p = arr;
printf("%d\n",p[2]);

结果为3.
p[2]等价于*(p+2)

int arr[] = {1,2,3,4};
int* p = arr + 3;
printf("%d\n",p[-1]);

结果为3.同理

指针减指针,同一数组内两指针之间相隔的元素数,也就是上方运算的逆运算
不同数组间不可以相减,因为位置会变的嘛

if(arr2 == arr1)
也是不相等的,两个数组首元素地址肯定不相等
比较字符串要用函数strcmp,并且需要包含头文件<string.h>

常用:
if(p == NULL)或者!=
不要用if(!p),此效果是判定是否为空,太难理解

int arr[] = {1,2,3,4};
printf(“%p\n”,arr + 1);
printf(“%p\n”,&arr + 1);

第一个指向2的地址
第二个指向4后一个元素,即整个数组后的一个元素
&arr叫做数组指针,是指针,指向一个数组的指针
数组指针也是一种指针,和int*,char*并列
数组也是一种基本的数据类型

void* 只知道地址,不关注大小

不能解引用
不能和整数相加减(因为不知道单个元素长度)
两个指针之间也无法相减

因为char与int类型不兼容,而void*类型的指针不关注大小,从而它可以保存各种不同类型的指针,泛型编程,一个参数可以给各种不同指针使用

const int* p;    p指向的内容不可修改
int const * p;   p指向的内容不可修改
int* const p;    p的指向不可修改
const int* const p;  两者都不可修改

const在前面,const在后面

二级指针也是一级指针,指向的也是指针而已
pp里存的是p的地址,p里又是num地址,解引用就得到了num地址里的内容
*pp是a的地址,**pp是a的内容

指针数组:
int* a[5];5个元素都是int*;

指针题:

int a[] = {1,2,3,4};
printf("%d\n",sizeof(a)); //16 数组大小
printf("%d\n",sizeof(*a)); //4 退化为指针,指向首元素,一个int元素长度为4
printf("%d\n",sizeof(a + 1)); //4 变成了指针
printf("%d\n",sizeof(a[1])); //4 去到了2,长度为4
printf("%d\n",sizeof(&a)); //4 数组指针本身还是4字节,指向的元素长度是16字节,这个指针里包含着的是地址,长度4字节
printf("%d\n",sizeof(*&a)); //16 由上一条,对地址解引用得到一个数组,sizeof就是16
printf("%d\n",sizeof(&a + 1)); //4 数组指针加整数,还是指针
printf("%d\n",sizeof(&a[0])); // 4 对首元素取地址,得到一个指向这里的指针

sizeof算的是内存strlen算的是字符串长度,从起始点到\n之间

解引用空指针会有什么后果?的三个水平回答:
1.程序崩溃
2.进程异常终止
3.硬件设备MMU发现访问了一个非法的虚拟地址,通知操作系统内核给进程发送11号信号,进程收到了一个11号信号,导致进程异常终止
以后会学到的↑

7.结构体

strlen 返回值是size_t,是一个无符号的长整型
%d是打印一个十进制有符号的整型
所以strlen要用%lu
%u表示无符号整数
%lu表示无符号长整型

GBK UTF-8

struct Student student1;
关键字 结构体类型 变量

strcpy(student.name,“lisi”);
前面放指针指向的数组,后面放字符,数组必须足够容纳字符,不得越界.
vs报错需要加宏或者_s,不推荐_s,因为这是微软造的,不是c自己的,Linux不支持,后面会讲更好的方法
只有c语言在赋值字符串的时候需要strcpy,后世都是用=

其他.
指针->,指针情况下按.也会变为->
后世语言都是用.

c里面结构体传参来回拷贝消耗太大,建议用指针传参
后世语言传参又简洁又不需要大开销

8.数据的存储

低位存储在低地址上,小端字节序
低位存储在高地址上,大端字节序

门牌号从左往右是101 102 103…这是不变的
如果数据在内存里存储是从101往高地址存,就是小端序,反过来就是大端序
大端序符合直觉, 00 00 00 FA,十六进制就是这样的,十进制也是12345元.
各人的电脑/操作系统 并不一定是大端或小端

整形在电脑中的存储:

32位系统–32位地址
正数的补码和原码相同,负数的补码是原码加一,反码是除符号位外其余的取反
计算机存储整形用补码,原因是硬件实现比较方便,计算机只用加法器就可以统一完成加减法,都是相加

数据在内存中具有一定的存储形式,存储同样的内容,可以按照不同的方式理解
如果两个数据在内存中存储形式是相同的,再按照相同的方式来理解,结果肯定也是相同的

+128:
1000 0000
8 0
-128:
1000 0000
0111 1111
1000 0000

无符号整形 (unsigned开头),不可能是负数
两个无符号整形相减,可能死循环,能不用就不用,用有符号整数代替

国际标准IEEE(电气和电子工程协会) 读作I triple E

10的-5次方在c语言里写作 1e-5

不能拿两个浮点数直接比较是否相等,因为浮点数的存储时有误差的(精度缺失),但十分微小
能不用浮点数,就不用浮点数,用整数.整数计算速度也比浮点数快.

为什么当赋给带符号类型一个超出它表示范围的值时,结果是未定义的?
原因在于内存读取机制。
在内存中,带符号类型首位是符号位,表示正负。假设此类型占8位。当读取超出范围的值时,将只取该数的最后8位进行读取,此时首位数字其实并不能代表正负,这样的读取没有意义,结果自然是未定义的。

9.指针的进阶

结果是第一行Func的地址
第二行hehe
第三行10
()是函数调用操作符,对于函数指针来说,最重要的操作是()

int (p)() = Func;
定义了一个指针变量p,p的类型是int(
)()
简化:
typedef int(*T)();
T p2 = Func;
使用typedef易于理解,不容易出错.

指针数组

函数指针数组

回调函数–函数调用时机不是由调用者决定的,往往是由操作系统或者是代码框架决定,因此叫回调.

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

最常见的地方就是排序,指定排序规则

信号,线程,都会用到回调函数.
后世的编程语言引入了比回调更有意义的概念叫 闭包.

10.字符串+内存函数

strlen返回值size_t
size_t是无符号长整型,要用%lu,即unsigned long首字母.
sizeof返回结果也是size_t
无符号长整型数相减可能会得到一个很大的数字?
能不用无符号数就不要用.

参数合法性校验:
方法1.函数内部进行判定,NULL
方法2.断言assert,加assert.h.如果表达式为真,断言通过,如果为假,断言失败,程序直接退出.

字符串只能使用=初始化,但是不能使用=赋值,要用strcpy
strcpy(destination,“hello”)需要你保证新值不大于原值
返回值为destination
%s对应类型为char , 即字符串. 用作输入时, 二者参数都要传char

strcat—concatenate拼接

char str1[] = "hello";
char str2[] = "world";
就得到了helloworld,拼接到了一起

strcmp
字符串是使用字符数组来表示的,数组名会隐式转换成指针,
if(str1 = str2)就是比较两个数组的首元素地址,肯定不相等
strcmp(str1,str2)结果:相等返回0,str1大于str2返回正数,str1小于str2返回负数

strstr

char str1[]="abcdefg";
char str2[]="cde";
char* p = strstr(str1,str2);

strstr就是在判定str2是不是str1的子字符串,

strtok字符串分割,用途:分割日志来找到需要的数据

strerror返回错误码所代表的错误原因
strerror(errno) errno.h

memcpy(,字节) 函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置
memmove,内存

11.自定义类型(结构体 枚举 联合)

typedef struct 结构体类型名{
成员列表;
}新的类型名;

匿名结构体一般没什么用

结构体的对齐规则:

  1. 第一个成员在与结构体变量偏移量为0的地址处。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
    对齐数 = 编译器默认的数 与 该成员大小的较小值。
    VS中默认的值为8 Linux中的默认值为4
  3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是 所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
    #pragma pack(1) //还原默认对齐数为1

联合体;各个变量起始地址都是从联合体起始地址开始
同一块内存里的数据可以有不同的理解模式.
可用于查看机器字节序.
和IP地址的转换
IP地址的表示方式:
1,点分十进制 192.168.1.1
2,用uint_t 的方式表示

12.动态内存管理

C语言最最重要的部分,面试考90%
书<<深入理解计算机系统>>

int* a = (int*)malloc(100* sizeof(int));
malloc只是动态申请一块连续的内存空间
1,申请内存空间的长度比较灵活
2,何时释放完全由用户掌控
free(a);//就释放了

如果开辟成功,则返回一个指向开辟好空间的指针。 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。

malloc生命周期在程序运行结束时
在内核里内存泄漏的话就要重启,提供的所有服务瞬间下线

如果频繁malloc但是忘记free,此时就是内存泄漏,内存是有限的.

java垃圾回收机制(GC)
1,优点:不用程序员关心啥时候释放内存
2,缺点:占用资源更多,开销更大(STW问题,stop the world)

int* p = (int*)malloc(4);
p = (int*)malloc(4);//重新开辟了新空间并把地址赋值给了p,释放时只释放了新地址,原来的没释放
free§;
//经典的内存泄漏
free必须搭配malloc(系列)函数使用
C++规定对空指针free/delete是合法的
推荐在free一个内存后把指针设置为NULL
malloc和free都在stdlib.h里,是标准库里的两个库函数
new/delete是C++中的两个运算符.

calloc 能初始化
realloc 动态扩容,当前位置不够的话就会另找位置并拷贝原有内容

int arr[100];//效率远远高于下一种
int* p = (int*)malloc(100* sizeof(int));

什么时候需要malloc内存?
当申请的内存比较大的时候需要malloc,局部变量存于栈中,栈的大小一般很小比如8M,看具体系统或自己设置,malloc在堆.
什么时候需要直接定义临时变量?
当对于内存申请的性能要求较高时

以上在虚拟内存,可以无限大
物理内存是电脑内存条插着的.

13.文件操作

光标放在库函数上,按f12进入库文件

内存在程序结束后就释放了,文件不会,文件存在磁盘上.
内存空间要申请,磁盘不用.

二进制文件和文本文件,文本文件看得懂,二进制文件看不懂

4个核心操作:fopen,fclose,fread,fwrite

FILE* fp =fopen(“./hello.txt”,“r”);//r是read
fopen返回FILE*----Linux会讲
每次打开都需要确认打开成功没:可能没这文件,可能空文件,可能没读/写权限

if(fp == NULL){
//printf(“%d\n”,strerror(errno));//+ errno.h + string.h,显示错误码,显示错误码的具体内容
perror(“fopen”);//!!!上一行的简写
return 1;
}
return 0;//main函数执行正确了就返回0,否则非0,“进程的退出码”
fp文件指针/文件的句柄 句柄----电视遥控器
文件在磁盘上没有地址,在内存上才有地址,文件指针(句柄)有地址.
size_t n = fread(buffer,1,1024,fp);

fclose(fp);
打开的文件不用了之后一定要及时关闭!
否则就是文件操作符泄漏/句柄泄漏/资源泄漏,可能导致后面无法再打开文件,一个进程能打开的文件数目有上限.
linux下ulimit -a 可查open file的数量,65535.可以设置.
每个程序一启动就会打开三个文件:标准输入stdin,标准输出stdout,标准错误stderr.----Linux会再讲

读取文件内容.从磁盘读取到内存中.所以需要一段内存:
malloc,定义变量
buffer 嗑瓜子,先把壳攒一手再去扔到垃圾桶.缓冲区,一段内存/寄存器/CPU缓存
char buf[1024] = {0};//初始化,每个元素都是0,和\0等同,则buf可视为字符串!字节长度可+1,取决于想读元素数写的大小,可能读不到.
fread(buf,1(每个元素的字节大小),4(想读几个元素),fp);//要写在打开后,关闭前.
返回值是成功读到的元素个数,可能比想读的要少的.

char buf[1024] = “aaaa”;//再改为bb后文件内容变成bb,w方式打开文件会先清空内容!!
fwrite(buf,1,1024,fp);//上面用"w",读写是"r+";
返回值是成功写入的元素个数.写入失败情况:磁盘空间不够,磁盘坏了
如果文件不存在,w方式下会创建一个新文件,而r是报错.
w是清空再写,a方式是追加写,不清空接着写入(换行写).

rwa 是文本文件
rb wb ab 打开二进制文件,binary

printf往标准输出中写格式化数据
fprintf往文件里写格式化数据
sprintf往内存中写格式化数据

数字转字符串:

char str[1024] = {0};
sprintf(str,“%d”,100);
printf(“%s\n”,str);
字符串转数字:

char str[1024] = {0};
int n = 0;
sscanf(str,“%d”,&n);
scanf是把标准输入(键盘)当作输入的内容
sscanf是从字符串里读数据

用函数转字符串与数字:
atoi/itoa a–ASCII–字符串 转 i–int–整数,浮点数就是f,长整型就是l

C++里还有3种方式.

文件的随机读写–内存的随机访问能力
移动文件光标位置:
fseek—定位文件指针
ftell—返回文件指针相对于起始位置的偏移量

文件结束判定:鸡肋
feof

14.编译过程+宏

C语言复习,还是需要多敲代码,自己踩坑再去解决它,才会懂
数据结构比C语言难度上一个台阶!
绘制一个C语言思维导图,写一篇博客.
秋招面试之前复习就是靠博客

C语言版本有:C89 C99 C11
公司一般用距今十年前的语言版本,为了稳定

编译原理==>如何实现一个编译器

VS是一个IDE,集成开发环境=编辑器+调试器+编译器+工程管理工具+…
其中编译器是MSVC,核心程序叫cl.exe

NULL是宏((void*)0),0被强转成void*

gcc编译C源代码过程:

1.预处理阶段 gcc -E hello.c -o hello.i
1)拷贝头文件 #是预处理指令符,带#的都在预处理阶段产生效果.
2)去掉注释 //
3)展开宏 #
4)处理条件编译 #
2.编译 gcc -S hello.i -o hello.s
源代码–>汇编代码(微机/单片机那种 MOV )
3.汇编 gcc -C hello.s -o hello.o
汇编代码–>二进制指令(@@@^)
4.链接
二进制指令–>二进制代码
会把依赖的其他代码都统一链接到一起,编译器对于每一个.c都是单独编译的,很多个.c得到了很多个.o,再加上一些链接库,被链接器融合成一个.exe

记忆:ESC键 iso国际标准化组织

条件编译:
#if 1

#else

#endif

宏功能:本质是文本替换
1)定义常量
2)定义类型
3)定义一段代码,使之类似于一个函数
C下:宏能搞定而函数搞不定的情况:
1)打印日志的时候需要带上文件名和行号__file__,line----C++也只能用这个
2)宏没有参数类型检查,所以同一个宏可以应用到不同类型的参数----C++模板
3)宏是直接展开,比函数调用开销更小—C++内联inline

C++不推荐使用宏,他的功能都有更好的替代方案,宏坑多难调试
在c++中,一般用const/枚举/内联去替代宏。

sizeof

在这里插入图片描述

结构体内存对齐

结构体的对齐规则:

  1. 第一个成员在与结构体变量偏移量为0的地址处。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
    VS中默认的值为8 Linux中的默认值为4
  3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

为什么存在内存对齐? 大部分的参考资料都是如是说的:

  1. 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址 处取某些特定类型的数据,否则抛出硬件异常。
  2. 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
    总体来说: 结构体的内存对齐是拿空间来换取时间的做法。

那在设计结构体的时候,我们既要满足对齐,又要节省空间,
如何做到: 让占用空间小的成员尽量集中在一起。

int main() {
       struct S3
       {
              char c1;//1*n=2
              char c2;//1*n=2
              int i;//4
       };
       struct Student
       {
              //12,未集中在一起时,对齐到最大对齐数
              //char c1;//1*n=4
              //int i;//4
              //char c2;//1*n=4

              //8,占用空间小的成员集中在一起可以又节省空间又保证了字节对齐
              //char c1;//1*n=2
              //char c2;//1*n=2
              //int i;//4

              //16,占用空间小的成员集中在一起可以又节省空间又保证了字节对齐
              //double d;//8
              //char c;//1*n=4
              //int i;//4*n=4

              //24,未集中在一起时,对齐到最大对齐数
              //int i;//4*n=8
              //double d;//8
              //char c;//1*n=8

              //32,构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
              char c;//1*n
              struct S3 s3;//8,4*n=8
              double d;//8*n=8
       };
       printf("%d\n", sizeof(struct Student));
       return 0;
}

assert使用方法

对于有好的变成习惯的程序员来说,使用assert是比较多的,可是,并不是所有的指针都可以用assert来断言,下边我来具体说一下,希望对那些不太会用断言的人有一点帮助~~

assert的正确使用:
assert是一个宏,原型定义在头文件assert.h中,在vs2015编译器下给出的定义如下:

#define assert(expression) (void)(
(!!(expression)) ||
(_wassert(_CRT_WIDE(#expression), _CRT_WIDE(FILE), (unsigned)(LINE)), 0)
)
在执行assert宏时,它会先计算出表达式expression的值,如果值为假,那么它想stderr打印出一条出错信息,通过调用abort来终止程序执行。

细心的你有可能会发现expression的前边怎么会有两个‘!’呢。是不是多余的呢??其实并不是。如果expression不是0,!!expression = =1,否则,!!expression = =0,这下你就知道了吧。为什么要这样做呢??这里用到了逻辑运算符’||'的短路功能。如果expression是真,后边的不执行,如果是假,打印出出错信息~~这个简直是妙~

看着assert好像是使用越多越好,其实并非这样。频繁的调用assert会极大地影响程序的性能,增加额外开销。并且它只能用在debug版本,release版本并不可。

在程序结束后,可以通过在包含#include<assert.h>的语句之前插入#define NDEBUG来禁用assert调用,比如(只给出头文件):
#include<stdio.h>
#define NDEBUG
#include<assert.h>

assert的用法总结:
(1)在函数开始处检验传入参入的合法性。这个我们平时使用的比较多。可是,如果某个条件在为假时,对于程序 而言仍为合法值,此时就不要assert断言了。(关于这个,在链表的博客中再述)
(2)每个assert最好只检验一个条件。否则,如果断言失败,我们无法直观的判断哪个条件失败。
(3)assert里的参数不能使用改变环境的语句。比如:
assert(i++<100);这条语句是不正确的,这是因为如果在这条语句执行之前i=100,
i++<100这个条件为假,程序直接报错,i++就不会执行。
(4)有的地方,assert不能代替条件过滤。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值