C语言基础知识


C语言关键字:

unsigned signed bool void char short int long double float struct enum union typedef sizeof
if else for switch case default while do break continue goto
extern register volatile auto const static return

常用的Linux系统命令:

touch/cat/more/head/tall/rm/cp/mv
mkdir/rmdir/cd/ls/tar
ifconfig/ping/telnet/ssh/ftp

vim文本编辑器:

在终端下依靠键盘操作使用的文本编辑器。
三大主要模式:正常模式、插入模式、命令模式
常用快捷键:Ctrl+x Ctrl+z Ctrl+c
教程:vimtutor

C语言介绍:

发明C语言的目的是什么
长盛不衰
优缺点、特点
C语言三剑客:《C语言陷阱和缺陷》、《C和指针》、《C专家编程》、《C程序设计语言》、

《C Primer Plus》、《必然》、《白说》

编译器介绍:

编译器就一个特殊的程序,把负责把C代码(文本文件)编译成可执行的二进制文件。
它由:预处理器、编译器、链接器组成
常用的参数:-E -c -S -o -std -l -D -Werror -Wall
E    激活预处理;头文件、宏等展开(.i文件)
S    激活预处理、编译;生成汇编代码(.s文件)
c    激活预处理、编译、汇编;生成目标文件(.o文件)
o    生成目标
Wall    打开编译告警(所有)
g    嵌入调试信息,方便gdb调试
《程序员的自我修养》

数据类型:

整型:unsigned、signed
char,short,int,long,long long
实型:float,double,long double
字符:char
布尔:bool
取值范围:char,short,int
各类型的字节数:
各类型的占位符:long double(%LF)

进制转换:

为什么需要二进制:因为现在的计算机由大规模集成电路构成,计算单元只能识别高低电流这种数据,因此只能使用二进制数据。
为什么需要八进制:为了方便记录二进制,由于历史原因八进制数据还在使用。
为什么需要十六进制:相当于升级版的八进制,由于计算机的高速发展,八进制已经无法满足需要。
十进制转换成二进制:
    求余:把十进制数据不停的用2求余,逆序记录求余的结果。
        189 % 2 = 1
        94 % 2 = 0
        47 % 2 = 1
        23 % 2 = 1
        11 % 2 = 1
        5 % 2 = 1
        2 % 2 = 0
        1 % 2 = 1
        10111101
    求权:128 64 32 16 8 4 2 1
二进制转换成十进制:
    2的不同次方相加。
二进制转换成八进制:三位二进制转换成一位八进制
    10 111 101 = 0275
    128 56 5 = 189
二进制转化成十六进制:四位二进制转换成一位十六进制
    10 <=> a
    1011 1101 = 0xbd
0b10111101 二进制
0275 八进制
0xbd 十六进制

数据在内存是如何存储的:
    数据分为原码、反码、补码,内存中存储的是数据的补码。
原码:数据直接转换成的二进制(无论正负)
反码:将原码按位求反得到反码
补码:
    正:原码
    负:反码+1
补码:10111101 char
    10111100
    01000011 -67

常量与变量

常量:程序运行过程中不可改变的数据
    字面值
    100 int类型
    100L long
    100U unsigned int
    100LU unsigned long
    100LL long long
    100LLU unsigned long long
    3.14 double
    3.14F float
    3.14LF long double
    枚举值
    宏常量
    具有const属性的被初始化过的全局变量
变量:类型 变量名;注意:取名规则,见名知意
    容器、数据

流程控制

if else,switch,for,while,do while,break,continue,goto
注意:大括号不要省略,分号不要多加
《C语言编码规范-华为》
《C++语言编程规范-谷歌》
如何判断XX类型是否是“零值”?
float,bool,int,char,int* p;
if(0.000001 > f && f >-0.000001)
if(flag)
if(0 == num)
if('\0' == ch)
if(NULL = p)
阅读、安全角度思考。

数组

C语言中只有一维数组,多维数组都是使用一维数组模拟的。
数组的越界:一切正常、段错误、脏数据。
char str[11] = "hello,world";
变长数组:数据的长度填写变量,编译时不能确定,程序运行期间可以变化,而执行数组定义语句时长度才固定下来。
    优点:可根据实际情况定义数组的长度,从而节约内存
    缺点:不能初始化。
int arr[10];
arr <=> int *
int arr[3][4]
arr <=> int (*)[4];

函数

函数是C语言中管理代码的最小单位,命名空间独立,栈空间独立。
函数被调用时开辟栈内存,函数结束后释放栈内存。

声明:返回值 函数名(类型1,类型2,...);
定义:返回值 函数名(类型1 参数名1,类型2 参数名2,...)
{
    函数体
}
注意:函数的定义如果出现调用之前,声明可以忽略
注意:调用函数时如果没有找到函数声明,也没有定义,编译器也不报错,而是先猜测函数的格式,链接时在尝试寻找函数的定义。
    return 语句只是把数据存储到一个特定的位置,当函数运行结束后,调用者就可以从这个位置获取到返回值。
        函数有返回值(格式),而没写return语句,调用者会得到一个不确定的返回值。

常见编译错误:
    隐式声明函数,没有找到函数声明和定义。1
    undefined reference to '',有函数声明,但无函数定义
函数的本质:函数就是存储在代码段中的一段数据(二进制指令的集合),函数名就是这段数据的开始位置
    因此函数名就是地址,可以定义指向这段数据的指针变量,
    返回值 (*函数指针) (类型1,类型2,...);

    函数的传参:
        1、只能值传递(内存拷贝),使用指针可提高效率(const int *)
        2、函数之间共享变量,全局变量(尽量少用),指针(注意安全)
        3、数组当作函数的参数时就蜕变成了指针(长度丢失),额外增加一个参数传递数组长度。

修饰变量的关键字

auto:用来修饰自动创建、释放的变量(局部变量、块变量),不加就代表加。
    注意:静态变量、全局变量不能用它来修饰。

static:
    限制作用域:全局变量、普通函数
    改变存储位置:把局部变量、块变量的存储位置由栈改为bss、data
    延长生命周期:把局部变量、块变量的生命周期延长与全局变量一样。
    static int fun(void);

const:为数据提供一种"保护"机制,变量被它修饰后就不能显示修改。
    也可以修饰函数的参数、返回值等。
    const int fun(void);

volatile:告诉编译器此变量的值不稳定、易变(不优化变量的取值)。
    多线程共享变量、硬件编程(裸机、驱动)

register:申请把变量的存储位置改为寄存器,但申请不一定成功
    注意:被它修饰过的变量不能取地址

extern:声明变量,用于不同.c之间共享全局变量(只能解决编译时问题)

注意:全局变量、局部变量、块变量的变量名可以同名,由于作用域不同,会互相屏蔽。
    块变量 > 局部变量 > 全局变量 (块变量屏蔽同名的局部变量)

程序在内存的分段:

代码段 test:存储的是代码所编译成的二进制指令、字符串字面值、常量
    具有只读属性,一旦修改会发生段错误。
全局数据段 data:初始化过的全局变量、静态变量
bss段(静态数据段):未初始化的全局变量、静态变量
    程序运行前会清理为0;
栈 stack:存储局部变量、块变量、大小有限,安全。
    由操作系统管理,以函数为单位使用(函数调用结束后自动释放)。
堆 heap:一般由程序员手动管理(让系统去映射),与指针配合使用,足够大,使用麻烦,释放的时间受控制
    但不安全,容易产生内存碎片、内存泄漏。

指针:

什么是指针:指针是一种数据类型(无符号整数,代表内存编号),使用它定义指针变量。
    0~4G(32个1)4294967295 byte
什么情况下使用指针:
    1、函数之间共享变量(全局变量有命名冲突,不会被释放,浪费内存)
    2、优化传递效率
        因为C语言采用的是值传递(内存拷贝),会随着变量字节数的增加而降低运行效率。
        而传递变量的地址,永远只拷贝4|8字节。
        void func(const int * p);
        但使用指针变量的值可能会被修改,可以配合const进行保护。    
    3、配合堆内存
如何使用指针:
    定义:类型 *变量名_p;
        1、与普通变量一样,默认值不确定,为了安全一般初始化NULL。
        2、一个*只能定义一个指针变量
            int *p1,*p2;
        3、指针变量与普通的用法不同,为了避免混用,一般从名字上加以区别。
        4、指针变量的类型决定了解决引用时访问的字节数。
    赋值:变量名_p = 地址;
        int* p = NULL;
        1、注意地址的类型
        2、void*可以与任意类型的指针进行自动转换(C++中不可以)
        3、要保障地址与物理内存有对应关系(映射过)。
    解引用:*p;
        根据指针变量中存储的内存编号,而访问内存中的数据。
        这个过程可以会有段错误,但这是由于赋值了有问题的地址。
使用指针要注意的问题:
    1、野指针:指向的目标不确定,解引用时不一定会出错,但未知的危险最可怕。
        而且野指针一旦产生就无法分辨,而预防的方法就是不制造野指针。
        1、定义指针时一定要初始化。
        2、指向的目标被释放后,要及时置空。
        3、不要指向随时可能被释放的目标。
    2、空指针:指针变量的值等于NULL,对这个地址解引用访问时,一定会产生段错误。
        因为它存储的是操作系统重启时所需要的数据。
        而预防的方法就是解引用前判断(来历不明) if(NULL == p)
指针的运算:
    指针+/-整数 = 指针+/-(宽度)*整数
    指针-指针 = (指针-指针)/宽度

指针与数组名:
    1、数组名就一个特殊的地址,它就代表数组的第一个元素的首地址,也能当指针使用。
        arr[i] <=> *(地址+i);
        因此指针也能使用[]运算符
    2、指针与目标内存是指向关系,而数组名是对应关系。
    3、数组当函数的参数就蜕变为了指针变量,长度丢失,安全性不保障。
        void fun(int* const arr,size_t len);
指针与const的配合使用:
    const int* p;
    int const * p;
    int * const p;
    const int * p;
    int const * const p;
指针的高级应用:
    指针数组:可以把无序的离散的数据,归纳到一起。
    数组指针:专门指向数组指针
    二级指针:指向指针的指针
    函数指针:指向函数的指针

字符串:

由字符组成的串型数据结构,它的结束标志是'\0'。
字符串存在的形式:
    字符数组:char arr[5] = {'a','b','c','d'};
        一般存储在栈,也可以存储在堆。
        要考虑'\0'的位置
    字符串字面值:由双引号包括的若干个字符,"hehe"。
        以地址形式存在,需要使用const char* str;指针指向。
        数据存在只读段,如果强行修改只会出现段错误。
        背后隐藏着'\0';
    char str[] = "hehe";
    一般使用字符串字面值来初始化字符数组。
字符串的输出:
    printf %s,puts,fprintf
字符串的输入:
    scanf %s:不能输入空格
    gets:不限制长度
    fgets:可能会接受到'\n',或者输入缓冲区中残留数据
字符串常见的操作:
    strlen/strcat/strcpy/strcmp
    strncat/strncpy/strncmp
    memset/memcpy/strstr/strchr
    sprintf/sscanf 用于拼接/解析字符串,非常好用
    字符数据 -> 数据 计算 数据 -> 字符数据

堆内存管理

C语言中没有内存管理的语句,只能借助标准库中的函数进行管理堆内存。
void *malloc(size_t size);
void free(void *ptr);
void *calloc(size_t nmemb, size_t size);
void *realloc(void *ptr, size_t size);

当向malloc首次申请内存时,malloc手中也没有内存,malloc会向系统申请,系统会映射33页内存交给malloc管理,
之后再向malloc申请内存,malloc会直接从33页内存中分配,直到33页用完,再向操作系统申请。
    访问malloc分配的内存时,可以越界,但不要超过33页范围。

内存泄漏:
    1、指针管理失误,指向其他位置。
    2、free语句没有执行到。
    3、free语句忘记写。
    就内存没释放,有申请新内存,导致可用的内存越来越少,速度越来越慢。
    前提:程序没有结束,当程序结束后属于它的所有资源都会被系统回收。
内存碎片:
    已经释放的内存,但不能被再次使用,这叫内存叫做内存碎片。
       内存碎片不是错误,它是由于内存的释放时间和分配时间不协调造成的。
       内存碎片无法避免(天然形成的),只能尽量减少:
           1、尽量使用栈内存,只有在数据量比较多的时候再使用堆内存。
           2、尽量申请大块内存自己管理。
           3、不要频繁的申请释放内存。

预处理指令:

把C代码翻译成标准的C代码叫预处理、负责翻译的程序叫预处理器、被翻译的代码叫预处理指令。
查看预处理的结果:
    gcc -E code.c 直接查看预处理的结果
    gcc -E code.c -o code.i 把预处理的结果保存到文件中
宏定义:
    宏常量:用一个有意义的单词代表一个字面值数据在代码中使用,在预处理时把单词替换成数据
        优点:提高可读性、安全、扩展方便
    宏函数:宏函数不是真正的函数,是带参数的宏,只是使用的方法类似函数
        预处理时参数会代入到表达式中,宏名会替换成后面的表达式。
        优点:运行速度快(没有参数传递),类型通用,只有极精简的代码段才适合定义宏函数
        缺点:不会进行类型检查,也没有返回值,只有一个计算结果,大量使用会增加代码段的冗余。
    预定义的宏:
        __FILENAME__
        __func__
        __DATE__
        __TIME__
        __LINE__
    条件编译:
        #if
        #elif
        #else
        #endif
        #ifndef
        #ifndef
        头文件卫士

复合数据类型:

结构 struct
设计数据类型
typedef struct Student
{
    char name[20];
    char sex;
    short age:1;
}Student;

定义结构变量
    Student stu;
    Student* stup = malloc(sizeof(Student));
访问结构成员
    stu.name,stu.sex,stu.age
    stup->name,stup->sex,stup->age
计算结构的字节数:
    注意:成员的顺序不同会影响结构的字节数。
    对齐:假定从零地址开始,每成员的起始地址编号,必须是它本身字节数的整数倍。
    补齐:结构的总字节数必须是它最大成员的整数倍。
    注意:在Linux系统下计算补齐、对齐时,成员超过4字节按4字节计算

联合 union
    从语法上来说与结构的用法基本类似,每个成员都从零地址开始,所有成员共有一块内存。
    1、使用联合判断大小端
    2、联合的总字节数计算,不需要对齐,但有补齐
枚举 enum
    值受限的int类型,把变量合法的值列举出来,除此以外不能等于其他的值
    枚举值是常量,可以直接使用在case后,常与switch语句配合使用

文件操作:

文件分类:
    文本文件:记录的是字符串的二进制
    二进制文件:直接把数据补码记录到文件中
文件打开:
    FILE *fopen(const char *path, const char *mode);
    "r"    以只读方式打开文件,如果文件不存在则打开失败,返回值为空。
    "r+" 在"r"的基础上增加写权限。
    "w" 以只写方式打开文件,如果文件不存在则创建,如果文件存在则把内容清空。
    "w+" 在"w"的基础上增加读取权限。
    "a" 以只写方式打开文件,如果文件不存在则创建,如果文件存在则把内容保留,与"w"区别是当有新数据写入,会追加到文件的末尾。
    "a+" 在"a"的基础上增加读权限。 
    "b" 在linux系统下没有用,表示以二进制格式打开文件。
        在Windows系统下不加b '\n' 写到文件中 系统会写入'\n\r',加b则写'\n'时只写入'\n'.
读写文本内容:
    int fprintf(FILE *stream, const char *format, ...);
    功能:把数据以文本形式写入到文件中
    stream:文件指针,fopen函数的返回值
    format:格式化控制符,点位符等
    ...:要写入的变量。
    返回值:成功写入的变量个数。    
    int fscanf(FILE *stream, const char *format, ...);
    功能:从文件中读取数据到变量,要求文件的内容是字符。
    stream:文件指针,fopen函数的返回值
    format:格式化控制符,点位符等
    ...:变量的地址
    返回值:成功匹配和赋值的个数,失败返回-1。        
读写二进制内容:
    size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);
    功能:内存中的数据,以二进制形式写入到文件中。
    ptr:要写入的内存的首地址
    size:要写入的字节数
    nmemb:要写入的次数
    stream:文件指针,fopen函数的返回值
    返回值:成功写入的次数    
    size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
    功能:从文件中以二进制方式读取数据到内存中。
    ptr:用来存放数据的内存首地址
    size:要读取的字节数
    nmemb:要读取的次数
    stream:文件指针,fopen函数的返回值
    返回值:成功读取的次数
文件位置指针:
    每个打开的文件系统都会用一个指针记录着它的读写位置,这个指针指向哪里,
    接下来对文件的读取就会从哪里继续,指针的位置会随着文件的读写自动发生变化
文件结构体中有一个成员记录文件的读写位置,称它位文件位置指针,有些情况下需要调整它的位置,获取到正确的数据。
    int fseek(FILE *stream, long offset, int whence);
    功能:根据基础位置+偏移值调整文件指针的位置。
    stream:文件指针,fopen函数的返回值
    offset:可以为正负,正往右(偏移值)
    whence:(基础位置)
        SEEK_SET 文件头
        SEEK_CUR 当前位置
        SEEK_END 文件尾
    long ftell(FILE *stream);
    功能:返回文件位置指针所在的位置。
    void rewind(FILE *stream);
    功能:把文件位置指针调整到开头
文件关闭:
    int fclose(FILE *fp);
    功能:把文件关闭,以释放相关资源,避免数据丢失。

多文件编程:

随着代码量的增加,不得不把代码分成若干个.c文件编写,这样能够给文件。。。
但缺点是不方便编译,需要借助编译脚本。
如何进行多文件编译:根据功能、责任分成若干个.c文件,然后为每个.c文件配备一个辅助文件.h然后单独编译每个.c文件,
    生成目标文件.o,然后再把.o文件合并成可执行文件。
头文件中应该写什么:
    1、头文件卫士
    2、宏常量、宏函数
    3、结构、联合、枚举的设计
    4、变量、函数的声明
    5、static函数的实现

编译脚本:

把用于编译的命令记录到文件中(makefile/Makefile),在终端里执行make程序时,make程序会自动读取当前目录中的。。。
make程序会监控每个文件的最后修改时间,如果没有被修改的文件不需要重新编译,这样可以节约大量的时间

注意:一定要使用tab缩进

GDB调试:

1、设置ubuntu系统,当段错误时产生core
    ulimit -c unlimited
2、编译时增加-g参数
3、再次执行新编译的程序,重新产生core文件
4、gdb a.out core 进行调试
    run/where

Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source Love丶伊卡洛斯 !
评论
 Previous
C++中类的继承(一) C++中类的继承(一)
一、类的继承1、共性与个性 表达不同类型事物之间公有的属性和行为。 个性用于刻画每种事物特有的属性和行为。 2、共性表示为父类(基类),个性表示为子类(派生类)。 子类继承自父类 基类派生出子类 二、继承的
2019-08-31
Next 
C++基础知识(三) C++基础知识(三)
一、this指针类的成员变量单独存储在每个类对象中,成员函数存储在代码段中,所有的类对象共享一份成员函数。 成员函数是如何区别调用它的是哪个类对象的? 答:借助了this指针,类的每个成员函数都有一个隐藏的参数this指针,它指向类对象。
2019-08-29
  TOC