C语言程序设计【提升篇】

1. 前言


 

  企业须要能干活的人,须要能上战场的兵。 node

1.1 技术层次

对于解决问题的解决方案有清晰的架构图,那么对于技术学习也要分清层次:linux

 

1.2 接口的封装设计

//初始化网络链接句柄socket,也叫环境初始化
int socketclient_init(void** handle);
 
//发送报文接口
int socketclient_send(void* handle, unsigned char* buf, int buflen);

//接收报文接口
int socketclient_recv(void* handle, unsigned char* buf, int* buflen);

//socket环境释放
int socketclient_destroy(void** handle);

1.3 过程的封装设计

 

//打印函数
void PrintArray(int arr[],int len)
{
//打印 for (int i = 0; i < len; i++)
{ printf(
"%d ", arr[i]); } printf("\n"); } void MySort(int arr[],int len) { //排序 for (int i = 0; i < len; i++)   { for (int j = len - 1; j > i; j--)   {   if (arr[j] < arr[j - 1]){   int temp = arr[j];   arr[j] = arr[j - 1];   arr[j - 1] = temp;  }   }  } }   void test()
{   
int arr[] = { 10, 50, 20, 90, 30 };   int len = sizeof(arr) / sizeof(int);   PrintArray(arr, len);   MySort(arr, len);   PrintArray(arr, len); }

1.4 战前准备

1.4.1 听课要求

  1. 专心听讲、积极思考;c++

  2. 遇到不懂的暂时先记下,课后再问;程序员

  3. 建议准备一个笔记本(记录重点、走神的时间);面试

  4. 当堂动手运行,不动手,永远学不会;编程

  5. 杜绝边听边敲(若是老师讲的知识点很熟,你能够边听边敲)、杜绝犯困听课。windows

  6. 若是时间容许,请课前作好预习;数组

  7. 从笔记、代码等资料中复习上课讲过的知识点。尽可能少回看视频,别对视频产生依赖,能够用2倍速度回看视频;缓存

  8. 按时完成老师布置的练习,记录练习中遇到的BUG和解决方案,根据本身的理解总结学到的知识点;网络

  9. 初学者 应该抓住重点,不要钻牛角尖遇到问题了,优先本身尝试解决,其次谷歌百度,最后再问老师;

  10. 若是时间容许,能够多去网上找对应阶段的学习资料面试题,注意做息,积极锻炼。

1.4.2 资料管理

 


 

2. C语言概述

  欢迎你们来到c语言的世界,c语言是一种强大的专业化的编程语言。

1.1 C语言的起源

  贝尔实验室的Dennis Ritchie在1972年开发了C,当时他正与ken Thompson一块儿设计UNIX操做系统,然而,C并非彻底由Ritchie构想出来的。它来自Thompson的B语言。

1.2 使用C语言的理由

  在过去的几十年中,c语言已成为最流行和最重要的编程语言之一。它之因此获得发展,是由于人们尝试使用它后都喜欢它。过去不少年中,许多人从c语言转而使用更强大的c++语言,但c有其自身的优点,仍然是一种重要的语言,并且它仍是学习c++的必经之路。

  • 高效性。c语言是一种高效的语言。c表现出一般只有汇编语言才具备的精细的控制能力(汇编语言是特定cpu设计所采用的一组内部制定的助记符。不一样的cpu类型使用不一样的汇编语言)。若是愿意,您能够细调程序以得到最大的速度或最大的内存使用率。

  • 可移植性。c语言是一种可移植的语言。意味着,在一个系统上编写的c程序通过不多改动或不通过修改就能够在其余的系统上运行。

  • 强大的功能和灵活性。c强大而又灵活。好比强大灵活的UNIX操做系统即是用c编写的。其余的语言(Perl、Python、BASIC、Pascal)的许多编译器和解释器也都是用c编写的。结果是当你在一台Unix机器上使用Python时,最终由一个c程序负责生成最后的可执行程序。

1.3 C语言标准

1.3.1 K&R C

  起初,C语言没有官方标准。1978年由美国电话电报公司(AT&T)贝尔实验室正式发表了C语言。布莱恩·柯林汉(Brian Kernighan) 和 丹尼斯·里奇(Dennis Ritchie) 出版了一本书,名叫《The C Programming Language》。这本书被 C语言开发者们称为K&R,不少年来被看成 C语言的非正式的标准说明。人们称这个版本的 C语言为K&R C

  K&R C主要介绍了如下特点:结构体struct)类型;长整数(long int)类型;无符号整数(unsigned int)类型;把运算符=+和=-改成+=和-=。由于=+和=-会使得编译器不知道使用者要处理i = -10仍是i =- 10,使得处理上产生混淆。

  即便在后来ANSI C标准被提出的许多年后,K&R C仍然是许多编译器的最准要求,许多老旧的编译器仍然运行K&R C的标准。

1.3.2 ANSI C/C89标准

  1970到80年代,C语言被普遍应用,从大型主机到小型微机,也衍生了C语言的不少不一样版本。1983年,美国国家标准协会(ANSI)成立了一个委员会X3J11,来制定 C语言标准。 

  1989年,美国国家标准协会(ANSI)经过了C语言标准,被称为ANSI X3.159-1989 "Programming Language C"。由于这个标准是1989年经过的,因此通常简称C89标准。有些人也简称ANSI C,由于这个标准是美国国家标准协会(ANSI)发布的。

  1990年,国际标准化组织ISO)和国际电工委员会IEC)把C89标准定为C语言的国际标准,命名为ISO/IEC 9899:1990 - Programming languages -- C[5]  。由于此标准是在1990年发布的,因此有些人把简称做C90标准。不过大多数人依然称之为C89标准,由于此标准与ANSI C89标准彻底等同。

  1994年,国际标准化组织(ISO)和国际电工委员会(IEC)发布了C89标准修订版,名叫ISO/IEC 9899:1990/Cor 1:1994[6]  ,有些人简称为C94标准

  1995年,国际标准化组织(ISO)和国际电工委员会(IEC)再次发布了C89标准修订版,名叫ISO/IEC 9899:1990/Amd 1:1995 - C Integrity[7]  ,有些人简称为C95标准

1.3.3 C99标准

  1999年1月,国际标准化组织(ISO)和国际电工委员会(IEC)发布了C语言的新标准,名叫ISO/IEC 9899:1999 - Programming languages -- C ,简称C99标准。这是C语言的第二个官方标准。

例如:

  • 增长了新关键字 restrict,inline,_Complex,_Imaginary,_Bool

  • 支持 long long,long double _Complex,float _Complex 这样的类型

  • 支持了不定长的数组。数组的长度就能够用变量了。声明类型的时候呢,就用 int a[*] 这样的写法。不过考虑到效率和实现,这玩意并非一个新类型。


 3. 内存分区

3.1 数据类型

3.1.1 数据类型概念

什么是数据类型?为何须要数据类型?
数据类型是为了更好进行内存的管理,让编译器能肯定分配多少内存。

  咱们现实生活中,狗是狗,鸟是鸟等等,每一种事物都有本身的类型,那么程序中使用数据类型也是来源于生活。 

  当咱们给狗分配内存的时候,也就至关于给狗建造狗窝,给鸟分配内存的时候,也就是给鸟建造一个鸟窝,咱们能够给他们各自建造一个别墅,可是会形成内存的浪费,不能很好的利用内存空间。

  咱们在想,若是给鸟分配内存,只须要鸟窝大小的空间就够了,若是给狗分配内存,那么也只须要狗窝大小的内存,而不是给鸟和狗都分配一座别墅,形成内存的浪费。

  当咱们定义一个变量,a = 10,编译器如何分配内存?计算机只是一个机器,它怎么知道用多少内存能够放得下10?

  因此说,数据类型很是重要,它能够告诉编译器分配多少内存能够放得下咱们的数据。

狗窝里面是狗,鸟窝里面是鸟,若是没有数据类型,你怎么知道冰箱里放得是一头大象!

数据类型基本概念:

类型是对数据的抽象;
类型相同的数据具备相同的表示形式、存储格式以及相关操做;
程序中全部的数据都一定属于某种数据类型;
数据类型能够理解为建立变量的模具: 固定大小内存的别名;

 

 

 3.1.2 数据类型别名

示例代码:

 

typedef unsigned int u32;
typedef struct _PERSON{
char name[64];
int age;
}Person;
void test(){
u32 val; //至关于 unsigned int val;
Person person; //至关于 struct PERSON person;
}

3.1.3 void数据类型 

  void字面意思是”无类型”,void* 无类型指针,无类型指针能够指向任何类型的数据。

  void定义变量是没有任何意义的,当你定义void a,编译器会报错。

  void真正用在如下两个方面:

    • 对函数返回的限定;
    • 对函数参数的限定;

示例代码:

//1. void修饰函数参数和函数返回
void test01(void){
printf("hello world");
}

//2. 不能定义void类型变量
void test02(){
void val; //报错
}

//3. void* 能够指向任何类型的数据,被称为万能指针
void test03(){
int a = 10;
void* p = NULL;
p = &a;
printf("a:%d\n",*(int*)p);
char c = 'a';
p = &c;
printf("c:%c\n",*(char*)p);
}

//4. void* 经常使用于数据类型的封装
void test04(){
//void * memcpy(void * _Dst, const void * _Src, size_t _Size);
}

3.1.4 sizeof操做符

  sizeof是c语言中的一个操做符,相似于++、--等等。sizeof可以告诉咱们编译器为某一特定数据或者某一个类型的数据在内存中分配空间时分配的大小,大小以字节为单位。

基本语法:

sizeof(变量);
sizeof 变量;
sizeof(类型);

sizeof 注意点: 

  • sizeof返回的占用空间大小是为这个变量开辟的大小,而不仅是它用到的空间。和现今住房的建筑面积和实用面积的概念差很少。因此对结构体用的时候,大多状况下就得考虑字节对齐的问题了;

  • sizeof返回的数据结果类型是unsigned int;

  • 要注意数组名和指针变量的区别。一般状况下,咱们总以为数组名和指针变量差很少,可是在用sizeof的时候差异很大,对数组名用sizeof返回的是整个数组的大小,而对指针变量进行操做的时候返回的则是指针变量自己所占得空间,在32位机的条件下通常都是4。并且当数组名做为函数参数时,在函数内部,形参也就是个指针,因此再也不返回数组的大小;

示例代码:

//1. sizeof基本用法
void test01(){
int a = 10;
printf("len:%d\n", sizeof(a));
printf("len:%d\n", sizeof(int));
printf("len:%d\n", sizeof a);
}
//2. sizeof 结果类型 void test02(){ unsigned int a = 10; if (a - 11 < 0){ printf("结果小于0\n"); } else{ printf("结果大于0\n");   } int b = 5; if (sizeof(b) - 10 < 0){ printf("结果小于0\n"); } else{ printf("结果大于0\n");   } } //3. sizeof 碰到数组 void TestArray(int arr[]){ printf("TestArray arr size:%d\n",sizeof(arr)); } void test03(){ int arr[] = { 10, 20, 30, 40, 50 }; printf("array size: %d\n",sizeof(arr)); //数组名在某些状况下等价于指针 int* pArr = arr; printf("arr[2]:%d\n",pArr[2]); printf("array size: %d\n", sizeof(pArr)); //数组作函数函数参数,将退化为指针,在函数内部再也不返回数组大小 TestArray(arr); }

3.1.5 数据类型总结

  • 数据类型本质是固定内存大小的别名,是个模具,C语言规定:经过数据类型定义变量;

  • 数据类型大小计算(sizeof);

  • 能够给已存在的数据类型起别名typedef;

  • 数据类型的封装(void 万能类型);

3.2 变量

3.1.1 变量的概念

  既能读又能写的内存对象,称为变量;若一旦初始化后不能修改的对象则称为常量。

变量定义形式: 类型  标识符, 标识符, … , 标识符

3.1.2 变量名的本质 

  • 变量名的本质:一段连续内存空间的别名;

  • 程序经过变量来申请和命名内存空间 int a = 0;

  • 经过变量名访问内存空间;

  • 不是向变量名读写数据,而是向变量所表明的内存空间中读写数据;

修改变量的两种方式:

void test(){
int a = 10;
//1. 直接修改
a = 20;
printf("直接修改,a:%d\n",a);

//2. 间接修改
int* p = &a;
*p = 30;
printf("间接修改,a:%d\n", a);
}

3.3 程序的内存分区模型

3.3.1 内存分区

3.3.1.1 运行以前

咱们要想执行咱们编写的c程序,那么第一步须要对这个程序进行编译。

1)预处理:宏定义展开、头文件展开、条件编译,这里并不会检查语法
2)编译:检查语法,将预处理后文件编译生成汇编文件
3)汇编:将汇编文件生成目标文件(二进制文件)
4)连接:将目标文件连接为可执行程序

  当咱们编译完成生成可执行文件以后,咱们经过在linux下size命令能够查看一个可执行二进制文件基本状况: 

  经过上图能够得知,在没有运行程序前,也就是说程序没有加载到内存前,可执行程序内部已经分好3段信息,分别为代码区(text)、数据区(data)和未初始化数据区(bss)3 个部分(有些人直接把data和bss合起来叫作静态区或全局区)。

代码区
存放 CPU 执行的机器指令。一般代码区是可共享的(即另外的执行程序能够调用它),使其可共享的目的是对于频繁被执行的程序,只须要在内存中有一份代码便可。代码区一般是只读的,使其只读的缘由是防止程序意外地修改了它的指令。另外,代码区还规划了局部变量的相关信息。
全局初始化数据区/静态数据区(data段)
该区包含了在程序中明确被初始化的全局变量、已经初始化的静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量)。

未初始化数据区(又叫 bss 区)
存入的是全局未初始化变量和未初始化静态变量。未初始化数据区的数据在程序开始执行以前被内核初始化为 0 或者空(NULL)。
整体来说说,程序源代码被编译以后主要分红两种段:程序指令和程序数据。代码段属于程序指令,而数据域段和.bss段属于程序数据。

那为何把程序的指令和程序数据分开呢?

程序被load到内存中以后,能够将数据和代码分别映射到两个内存区域。因为数据区域对进程来讲是可读可写的,而指令区域对程序来说说是只读的,因此分区以后呢,能够将程序指令区域和数据区域分别设置成可读可写或只读。这样能够防止程序的指令有意或者无心被修改;
当系统中运行着多个一样的程序的时候,这些程序执行的指令都是同样的,因此只须要内存中保存一份程序的指令就能够了,只是每个程序运行中数据不同而已,这样能够节省大量的内存。好比说以前的Windows Internet Explorer 7.0运行起来以后, 它须要占用112 844KB的内存,它的私有部分数据有大概15 944KB,也就是说有96 900KB空间是共享的,若是程序中运行了几百个这样的进程,能够想象共享的方法能够节省大量的内存。
3.3.1.2 运行以后 

  程序在加载到内存前,代码区和全局区(data和bss)的大小就是固定的,程序运行期间不能改变。而后,运行可执行程序,操做系统把物理硬盘程序load(加载)到内存,除了根据可执行程序的信息分出代码区(text)、数据区(data)和未初始化数据区(bss)以外,还额外增长了栈区、堆区。

代码区(text segment)
加载的是可执行文件代码段,全部的可执行代码都加载到代码区,这块内存是不能够在运行期间修改的。

未初始化数据区(BSS)
加载的是可执行文件BSS段,位置能够分开亦能够紧靠数据段,存储于数据段的数据(全局未初始化,静态未初始化数据)的生存周期为整个程序运行过程。

全局初始化数据区/静态数据区(data segment)
加载的是可执行文件数据段,存储于数据段(全局初始化,静态初始化数据,文字常量(只读))的数据的生存周期为整个程序运行过程。

栈区(stack)
栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程当中实时加载和释放,所以,局部变量的生存周期为申请到释放该段栈空间。
 
堆区(heap)
堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于BSS区和栈区之间。通常由程序员分配和释放,若程序员不释放,程序结束时由操做系统回收。

类型

做用域

生命周期

存储位置

auto变量

一对{}内

当前函数

栈区

static局部变量

一对{}内

整个程序运行期

初始化在data段,未初始化在BSS段

extern变量

整个程序

整个程序运行期

初始化在data段,未初始化在BSS段

static全局变量

当前文件

整个程序运行期

初始化在data段,未初始化在BSS段

extern函数

整个程序

整个程序运行期

代码区

static函数

当前文件

整个程序运行期

代码区

register变量

一对{}内

当前函数

运行时存储在CPU寄存器

字符串常量

当前文件

整个程序运行期

data段

注意:创建正确程序运行内存布局图是学好C的关键!!

3.3.2 分区模型 

3.3.2.1 栈区

  由系统进行内存的管理。主要存放函数的参数以及局部变量。在函数完成执行,系统自行释放栈区内存,不须要用户管理。

 

#char* func(){
char p[] = "hello world!"; //在栈区存储 乱码
printf("%s\n", p);
return p;
}
void test(){
char* p = NULL;
p = func();  
printf("%s\n",p); 
}

 

3.3.2.2 堆区

  由编程人员手动申请,手动释放,若不手动释放,程序结束后由系统回收,生命周期是整个程序运行期间。使用malloc或者new进行堆的申请。

char* func()
{
char* str = malloc(100);
strcpy(str, "hello world!");
printf("%s\n",str);
return str;
}
void test01()
{
char* p = NULL;
p = func();
printf("%s\n",p);
}
void allocateSpace(char* p)
{
p = malloc(100);
strcpy(p, "hello world!");
printf("%s\n", p);
}
void test02()
{
char* p = NULL;
allocateSpace(p);
printf("%s\n", p);
}

 

堆分配内存API:

#include <stdlib.h>
void *calloc(size_t nmemb, size_t size);

功能:
在内存动态存储区中分配nmemb块长度为size字节的连续区域。calloc自动将分配的内存 置0。

参数:
nmemb:所需内存单元数量
size:每一个内存单元的大小(单位:字节)

返回值:
成功:分配空间的起始地址
失败:NULL

 

#include <stdlib.h>
void *realloc(void *ptr, size_t size);

功能:
从新分配用malloc或者calloc函数在堆中分配内存空间的大小。
realloc不会自动清理增长的内存,须要手动清理,若是指定的地址后面有连续的空间,那么就会在已有地址基础上增长内存,若是指定的地址后面没有空间,那么realloc会从新分配新的连续内存,把旧内存的值拷贝到新内存,同时释放旧内存。

参数:
ptr:为以前用malloc或者calloc分配的内存地址,若是此参数等于NULL,那么和realloc与malloc功能一致
size:为从新分配内存的大小, 单位:字节
返回值:
成功:新分配的堆内存地址
失败:NULL

示例代码: 

void test01(){ 
int* p1 = calloc(10,sizeof(int));
if (p1 == NULL){
return;
}
for (int i = 0; i < 10; i ++){
p1[i] = i + 1;
}
for (int i = 0; i < 10; i++){
printf("%d ",p1[i]);
}
printf("\n");
free(p1);
}
void test02(){
int* p1 = calloc(10, sizeof(int));
if (p1 == NULL){
return;
}
for (int i = 0; i < 10; i++){
p1[i] = i + 1;
}
int* p2 = realloc(p1, 15 * sizeof(int));
if (p2 == NULL){
return;
}
printf("%d\n", p1);
printf("%d\n", p2);
//打印
for (int i = 0; i < 15; i++){
printf("%d ", p2[i]);
}
printf("\n");
//从新赋值
for (int i = 0; i < 15; i++){
p2[i] = i + 1;
}
//再次打印
for (int i = 0; i < 15; i++){
printf("%d ", p2[i]);
}
printf("\n");
free(p2);
}

3.3.2.3 全局/静态区

 

  全局静态区内的变量在编译阶段已经分配好内存空间并初始化。这块内存在程序运行期间一直存在,它主要存储全局变量静态变量常量

 

注意

(1)这里不区分初始化和未初始化的数据区,是由于静态存储区内的变量若不显示初始化,则编译器会自动以默认的方式进行初始化,即静态存储区内不存在未初始化的变量。

(2)全局静态存储区内的常量分为常变量和字符串常量,一经初始化,不可修改。静态存储内的常变量是全局变量,与局部常变量不一样,区别在于局部常变量存放于栈,实际可间接经过指针或者引用进行修改,而全局常变量存放于静态常量区则不能够间接修改。

(3)字符串常量存储在全局/静态存储区的常量区。 

示例代码

int v1 = 10;//全局/静态区
const int v2 = 20; //常量,一旦初始化,不可修改
static int v3 = 20; //全局/静态区
char *p1; //全局/静态区,编译器默认初始化为NULL
//那么全局static int 和 全局int变量有什么区别?
void test(){
static int v4 = 20; //全局/静态区
}
加深理解

char
* func(){ static char arr[] = "hello world!"; //在静态区存储 可读可写 arr[2] = 'c'; char* p = "hello world!"; //全局/静态区-字符串常量区 //p[2] = 'c'; //只读,不可修改 printf("%d\n",arr); printf("%d\n",p); printf("%s\n", arr); return arr; } void test(){ char* p = func(); printf("%s\n",p); }

字符串常量是否可修改?字符串常量优化:

ANSI C中规定:修改字符串常量,结果是未定义的。

ANSI C并无规定编译器的实现者对字符串的处理,例如:

1.有些编译器可修改字符串常量,有些编译器则不可修改字符串常量。

2.有些编译器把多个相同的字符串常量当作一个(这种优化可能出如今字符串常量中,节省空间),有些则不进行此优化。若是进行优化,则可能致使修改一个字符串常量致使另外的字符串常量也发生变化,结果不可知。

因此尽可能不要去修改字符串常量

C99标准:

char *p = "abc"; defines p with type ‘‘pointer to char’’ and initializes it to point to an object with type ‘‘array of char’’ with length 4 whose elements are initialized with a character string literal. If an attempt is made to use p to modify the contents of the array, the behavior is undefined.

 

字符串常量地址是否相同

tc2.0,同文件字符串常量地址不一样

Vs2013,字符串常量地址同文件和不一样文件都相同

Dev c++、QT同文件相同不一样文件不一样

3.3.2.4 总结

  在理解C/C++内存分区时,常会碰到以下术语:数据区,堆,栈,静态区,常量区,全局区,字符串常量区,文字常量区,代码区等等,初学者被搞得云里雾里。在这里,尝试捋清楚以上分区的关系。

  数据区包括:堆,栈,全局/静态存储区。
  全局/静态存储区包括:常量区,全局区、静态区。
  常量区包括:字符串常量区、常变量区。
  代码区:存放程序编译后的二进制代码,不可寻址区。

  能够说,C/C++内存分区其实只有两个,即代码区和数据区。

3.3.3 函数调用模型

3.3.3.1 函数调用流程

  栈(stack)是现代计算机程序里最为重要的概念之一,几乎每个程序都使用了栈,没有栈就没有函数,没有局部变量,也就没有咱们现在能见到的全部计算机的语言。在解释为何栈如此重要以前,咱们先了解一下传统的栈的定义:

  在经典的计算机科学中,栈被定义为一个特殊的容器,用户能够将数据压入栈中(入栈,push),也能够将压入栈中的数据弹出(出栈,pop),可是栈容器必须遵循一条规则:先入栈的数据最后出栈(First In First Out,FIFO).

  在经典的操做系统中,栈老是向下增加的。压栈的操做使得栈顶的地址减少,弹出操做使得栈顶地址增大。

  栈在程序运行中具备极其重要的地位。最重要的,栈保存一个函数调用所须要维护的信息,这一般被称为堆栈帧(Stack Frame)或者活动记录(Activate Record).一个函数调用过程所须要的信息通常包括如下几个方面:

函数的返回地址;
函数的参数;
临时变量;
保存的上下文:包括在函数调用先后须要保持不变的寄存器。

咱们从下面的代码,分析如下函数的调用过程:

 

int func(int a,int b){
int t_a = a;
int t_b = b;
return t_a + t_b;
}
int main(){
int ret = func(10, 20);
int a = 20;
return EXIT_SUCCESS;
}

 

 

 

 

3.3.3.2 调用惯例 

     如今,咱们大体了解了函数调用的过程,这期间有一个现象,那就是函数的调用者和被调用者对函数调用有着一致的理解,例如,它们双方都一致的认为函数的参数是按照某个固定的方式压入栈中。若是不这样的话,函数将没法正确运行。

     若是函数调用方在传递参数的时候先压入a参数,再压入b参数,而被调用函数则认为先压入的是b,后压入的是a,那么被调用函数在使用a,b值时候,就会颠倒。

所以,函数的调用方和被调用方对于函数是如何调用的必须有一个明确的约定,只有双方都遵循一样的约定,函数才可以被正确的调用,这样的约定被称为”调用惯例(Calling Convention)”.一个调用惯例通常包含如下几个方面:

函数参数的传递顺序和方式

函数的传递有不少种方式,最多见的是经过栈传递。函数的调用方将参数压入栈中,函数本身再从栈中将参数取出。对于有多个参数的函数,调用惯例要规定函数调用方将参数压栈的顺序:从左向右,仍是从右向左。有些调用惯例还容许使用寄存器传递参数,以提升性能。

栈的维护方式

在函数将参数压入栈中以后,函数体会被调用,此后须要将被压入栈中的参数所有弹出,以使得栈在函数调用先后保持一致。这个弹出的工做能够由函数的调用方来完成,也能够由函数自己来完成。

为了在连接的时候对调用惯例进行区分,调用惯例要对函数自己的名字进行修饰。不一样的调用惯例有不一样的名字修饰策略。

事实上,在c语言里,存在着多个调用惯例,而默认的是cdecl.任何一个没有显示指定调用惯例的函数都是默认是cdecl惯例。好比咱们上面对于func函数的声明,它的完整写法应该是:

 int _cdecl func(int a,int b);

注意: _cdecl不是标准的关键字,在不一样的编译器里可能有不一样的写法,例如gcc里就不存在_cdecl这样的关键字,而是使用__attribute__((cdecl)).

 

调用惯例

出栈方

参数传递

名字修饰

cdecl

函数调用方

从右至左参数入栈

下划线+函数名

stdcall

函数自己

从右至左参数入栈

下划线+函数名+@+参数字节数

fastcall

函数自己

前两个参数由寄存器传递,其他参数经过堆栈传递。

@+函数名+@+参数的字节数

pascal

函数自己

从左至右参数入栈

较为复杂,参见相关文档

3.3.3.2 函数变量传递分析

 

 

 

 

3.3.4 栈的生长方向和内存存放方向

 

//1. 栈的生长方向
void test01(){
int a = 10;
int b = 20;
int c = 30;
int d = 40;
printf("a = %d\n", &a);
printf("b = %d\n", &b);
printf("c = %d\n", &c);
printf("d = %d\n", &d);
//a的地址大于b的地址,故而生长方向向下
}

//2. 内存生长方向(小端模式)
void test02(){
//高位字节 -> 地位字节
int num = 0xaabbccdd;
unsigned char* p = &num;

//从首地址开始的第一个字节
printf("%x\n",*p);
printf("%x\n", *(p + 1));
printf("%x\n", *(p + 2));
printf("%x\n", *(p + 3));
}

4. 指针强化 

4.1 指针是一种数据类型

4.1.1 指针变量

  指针是一种数据类型,占用内存空间,用来保存内存地址。

void test01(){
int* p1 = 0x1234;
int*** p2 = 0x1111;
printf("p1 size:%d\n",sizeof(p1));
printf("p2 size:%d\n",sizeof(p2));

//指针是变量,指针自己也占内存空间,指针也能够被赋值
int a = 10;
p1 = &a;
printf("p1 address:%p\n", &p1);
printf("p1 address:%p\n", p1);
printf("a address:%p\n", &a);
}

4.1.2 空指针野指针

①空指针

 

  标准定义了NULL指针,它做为一个特殊的指针变量,表示不指向任何东西。要使一个指针为NULL,能够给它赋值一个零值。为了测试一个指针百年来那个是否为NULL,你能够将它与零值进行比较。

  对指针解引用操做能够得到它所指向的值。但从定义上看,NULL指针并未执行任何东西,由于对一个NULL指针因引用是一个非法的操做,在解引用以前,必须确保它不是一个NULL指针。

 若是对一个NULL指针间接访问会发生什么呢?结果因编译器而异。

  不容许向NULL和非法地址拷贝内存

void test(){
char *p = NULL;
//给p指向的内存区域拷贝内容
strcpy(p, "1111"); //err
char *q = 0x1122;
//给q指向的内存区域拷贝内容
strcpy(q, "2222"); //err
}

② 野指针 

在使用指针时,要避免野指针的出现:

  野指针指向一个已删除的对象或未申请访问受限内存区域的指针。与空指针不一样,野指针没法经过简单地判断是否为 NULL避免,而只能经过养成良好的编程习惯来尽力减小。对野指针进行操做很容易形成程序错误。

什么状况下回致使野指针?

指针变量未初始化

任何指针变量刚被建立时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。因此,指针变量在建立的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。

指针释放后未置空

有时指针在free或delete后未赋值 NULL,便会令人觉得是合法的。别看free和delete的名字(尤为是delete),它们只是把指针所指的内存给释放掉,但并无把指针自己干掉。此时指针指向的就是“垃圾”内存。释放后的指针应当即将指针置为NULL,防止产生“野指针”。

指针操做超越变量做用域

不要返回指向栈内存的指针或引用,由于栈内存在函数结束时会被释放。

void test(){
int* p = 0x001; //未初始化
printf("%p\n",p);
*p = 100;
}

操做野指针是很是危险的操做,应该规避野指针的出现: 

初始化时置 NULL

指针变量必定要初始化为NULL,由于任何指针变量刚被建立时不会自动成为NULL指针,它的缺省值是随机的。

释放时置 NULL

当指针p指向的内存空间释放时,没有设置指针p的值为NULL。delete和free只是把内存空间释放了,可是并无将指针p的值赋为NULL。一般判断一个指针是否合法,都是使用if语句测试该指针是否为NULL。

4.1.3 间接访问操做符

  经过一个指针访问它所指向的地址的过程叫作间接访问,或者叫解引用指针,这个用于执行间接访问的操做符是*。

  注意:对一个int*类型指针解引用会产生一个整型值,相似地,对一个float*指针解引用会产生了一个float类型的值。

int arr[5];
int *p = * (&arr);
int arr1[5][3] arr1 = int(*)[3]
&arr1

 在指针声明时,* 号表示所声明的变量为指针

在指针使用时,* 号表示操做指针所指向的内存空间

1)* 至关经过地址(指针变量的值)找到指针指向的内存,再操做内存

2)* 放在等号的左边赋值(给内存赋值,写内存)

3)* 放在等号的右边取值(从内存中取值,读内存)

//解引用
void test01(){
//定义指针
int* p = NULL;
//指针指向谁,就把谁的地址赋给指针
int a = 10;
p = &a;
*p = 20;//*在左边当左值,必须确保内存可写
//*号放右面,从内存中读值
int b = *p;
//必须确保内存可写
char* str = "hello world!";
*str = 'm';
printf("a:%d\n", a);
printf("*p:%d\n", *p);
printf("b:%d\n", b);
}

4.1.4 指针的步长

  指针是一种数据类型,是指它指向的内存空间的数据类型。指针所指向的内存空间决定了指针的步长。指针的步长指的是,当指针+1时候,移动多少字节单位。

思考以下问题:

int a = 0xaabbccdd;
unsigned int *p1 = &a;
unsigned char *p2 = &a;
//为何*p1打印出来正确结果?
printf("%x\n", *p1);
//为何*p2没有打印出来正确结果?
printf("%x\n", *p2);
//为何p1指针+1加了4字节?
printf("p1  =%d\n", p1);
printf("p1+1=%d\n", p1 + 1);
//为何p2指针+1加了1字节?
printf("p2  =%d\n", p2);
printf("p2+1=%d\n", p2 + 1);

4.2 指针的意义_间接赋值

 

4.2.1 间接赋值的三大条件

 

经过指针间接赋值成立的三大条件:

  • 1)2个变量(一个普通变量一个指针变量、或者一个实参一个形参)

  • 2)创建关系

  • 3)经过 * 操做指针指向的内存

void test(){
int a = 100;    //两个变量
int *p = NULL;
//创建关系
//指针指向谁,就把谁的地址赋值给指针
p = &a;
//经过*操做内存
*p = 22;
}
 4.2.2 如何定义合适的指针变量

 

void test(){
int b;  
int *q = &b; //0级指针
int **t = &q;
int ***m = &t;
}

 

4.2.3 间接赋值:从0级指针到1级指针
int func1(){ return 10; }
void func2(int a){
a = 100;
}
//指针的意义_间接赋值
void test02(){
int a = 0;
a = func1();
printf("a = %d\n", a);

//为何没有修改?
func2(a);
printf("a = %d\n", a);
}
//指针的间接赋值
void func3(int* a){
*a = 100;
}
void test03(){
int a = 0;
a = func1();
printf("a = %d\n", a);
//修改
func3(&a);
printf("a = %d\n", a);
}
4.2.4 间接赋值:从1级指针到2级指针 
void AllocateSpace(char** p){
*p = (char*)malloc(100);
strcpy(*p, "hello world!");
}
void FreeSpace(char** p){
if (p == NULL){
return;
}
if (*p != NULL){
free(*p);
*p = NULL;
}
}
void test(){
char* p = NULL;
AllocateSpace(&p);
printf("%s\n",p);
FreeSpace(&p);
if (p == NULL){
printf("p内存释放!\n");
}
}

4.2.4 间接赋值的推论 

  • 1级指针形参,去间接修改了0级指针(实参)的值。

  • 2级指针形参,去间接修改了1级指针(实参)的值。

  • 3级指针形参,去间接修改了2级指针(实参)的值。

  • n级指针形参,去间接修改了n-1级指针(实参)的值。

4.3 指针作函数参数

  指针作函数参数,具有输入和输出特性:

  • 输入:主调函数分配内存

  • 输出:被调用函数分配内存

4.3.1 输入特性

void fun(char *p /* in */)
{
//给p指向的内存区域拷贝内容
strcpy(p, "abcddsgsd");
}
void test(void)
{
//输入,主调函数分配内存
char buf[100] = { 0 };
fun(buf);
printf("buf  = %s\n", buf);
}
4.3.2 输出特性 
void fun(char **p /* out */, int *len)
{
char *tmp = (char *)malloc(100);
if (tmp == NULL)
{
return;
}
strcpy(tmp, "adlsgjldsk");
//间接赋值
*p = tmp;
*len = strlen(tmp);
}

void test(void)
{
//输出,被调用函数分配内存,地址传递
char *p = NULL;
int len = 0;
fun(&p, &len);
if (p != NULL)
{
printf("p = %s, len = %d\n", p, len);
}

4.4 字符串指针强化

4.4.1 字符串指针作函数参数

4.4.1.1 字符串基本操做
//字符串基本操做
//字符串是以0或者'\0'结尾的字符数组,(数字0和字符'\0'等价)
void test01(){

//字符数组只能初始化5个字符,当输出的时候,从开始位置直到找到0结束
char str1[] = { 'h', 'e', 'l', 'l', 'o' };
printf("%s\n",str1);

//字符数组部分初始化,剩余填0
char str2[100] = { 'h', 'e', 'l', 'l', 'o' };
printf("%s\n", str2);

//若是以字符串初始化,那么编译器默认会在字符串尾部添加'\0'
char str3[] = "hello";
printf("%s\n",str3);
printf("sizeof str:%d\n",sizeof(str3));
printf("strlen str:%d\n",strlen(str3));

//sizeof计算数组大小,数组包含'\0'字符
//strlen计算字符串的长度,到'\0'结束
//那么若是我这么写,结果是多少呢?
char str4[100] = "hello";
printf("sizeof str:%d\n", sizeof(str4));
printf("strlen str:%d\n", strlen(str4));

//请问下面输入结果是多少?sizeof结果是多少?strlen结果是多少?
char str5[] = "hello\0world"; 
printf("%s\n",str5);
printf("sizeof str5:%d\n",sizeof(str5));
printf("strlen str5:%d\n",strlen(str5));

//再请问下面输入结果是多少?sizeof结果是多少?strlen结果是多少?
char str6[] = "hello\012world";
printf("%s\n", str6);
printf("sizeof str6:%d\n", sizeof(str6));
printf("strlen str6:%d\n", strlen(str6));
}

八进制和十六进制转义字符:

     在C中有两种特殊的字符,八进制转义字符和十六进制转义字符,八进制字符的通常形式是'\ddd',d是0-7的数字。十六进制字符的通常形式是'\xhh',h是0-9或A-F内的一个。八进制字符和十六进制字符表示的是字符的ASCII码对应的数值。

好比 :

n '\063'表示的是字符'3',由于'3'的ASCII码是30(十六进制),48(十进制),63(八进制)。

n '\x41'表示的是字符'A',由于'A'的ASCII码是41(十六进制),65(十进制),101(八进制)。

4.4.1.2 字符串拷贝功能实现
//拷贝方法1
void copy_string01(char* dest, char* source ){
for (int i = 0; source[i] != '\0';i++){
dest[i] = source[i];
}
}
//拷贝方法2
void copy_string02(char* dest, char* source){
while (*source != '\0' /* *source != 0 */){
*dest = *source;
source++;
dest++;
}
}
//拷贝方法3
void copy_string03(char* dest, char* source){
//判断*dest是否为0,0则退出循环
while (*dest++ = *source++){}
}
//拷贝方法4
//1)应该判断下传入的参数是否为NULL
//2)最好不要直接使用形参
int copy_string04(char* dest, char* source){
if (dest == NULL){
return -1;
}
if (source == NULL){
return -2;
}
char* src = source;
char* tar = dest;
while (*tar++ = *src++){}
return 0;
}
4.4.1.3 字符串反转模型

void reverse_string(char* str){
if (str == NULL){
return;
}
int begin = 0;
int end = strlen(str) - 1;
while (begin < end){
//交换两个字符元素
char temp = str[begin];
str[begin] = str[end];
str[end] = temp;
begin++;
end--;
}
}
void test(){
char str[] = "abcdefghijklmn";
printf("str:%s\n", str);
reverse_string(str);
printf("str:%s\n", str);
}

4.4.2 字符串的格式化 

4.4.2.1 sprintf
#include <stdio.h>
int sprintf(char *str, const char *format, ...);
功能:
     根据参数format字符串来转换并格式化数据,而后将结果输出到str指定的空间中,直到    出现字符串结束符 '\0'  为止。
参数:
str:字符串首地址
format:字符串格式,用法和printf()同样
返回值:
成功:实际格式化的字符个数
失败: - 1
void test(){
//1. 格式化字符串
char buf[1024] = { 0 };
sprintf(buf, "你好,%s,欢迎加入咱们!", "John");
printf("buf:%s\n",buf);
memset(buf, 0, 1024);
sprintf(buf, "我今年%d岁了!", 20);
printf("buf:%s\n", buf);

//2. 拼接字符串
memset(buf, 0, 1024);
char str1[] = "hello";
char str2[] = "world";
int len = sprintf(buf,"%s %s",str1,str2);
printf("buf:%s len:%d\n", buf,len);

//3. 数字转字符串
memset(buf, 0, 1024);
int num = 100;
sprintf(buf, "%d", num);
printf("buf:%s\n", buf);

//设置宽度 右对齐
memset(buf, 0, 1024);
sprintf(buf, "%8d", num);
printf("buf:%s\n", buf);

//设置宽度 左对齐
memset(buf, 0, 1024);
sprintf(buf, "%-8d", num);
printf("buf:%s\n", buf);

//转成16进制字符串 小写
memset(buf, 0, 1024);
sprintf(buf, "0x%x", num);
printf("buf:%s\n", buf);

//转成8进制字符串
memset(buf, 0, 1024);
sprintf(buf, "0%o", num);
printf("buf:%s\n", buf);
}

4.4.2.2 sscanf 

#include <stdio.h>
int sscanf(const char *str, const char *format, ...);
功能:
    从str指定的字符串读取数据,并根据参数format字符串来转换并格式化数据。
参数:
str:指定的字符串首地址
format:字符串格式,用法和scanf()同样
返回值:
成功:实际读取的字符个数
失败: - 1

格式

做用

%*s或%*d

跳过数据

%[width]s

读指定宽度的数据

%[a-z]

匹配a到z中任意字符(尽量多的匹配)

%[aBc]

匹配a、B、c中一员,贪婪性

%[^a]

匹配非a的任意字符,贪婪性

%[^a-z]

表示读取除a-z之外的全部字符

 

//1. 跳过数据
void test01(){
char buf[1024] = { 0 };
//跳过前面的数字
//匹配第一个字符是不是数字,若是是,则跳过
//若是不是则中止匹配
sscanf("123456aaaa", "%*d%s", buf); 
printf("buf:%s\n",buf);
}

//2. 读取指定宽度数据
void test02(){
char buf[1024] = { 0 };
//跳过前面的数字
sscanf("123456aaaa", "%7s", buf);
printf("buf:%s\n", buf);
}

//3. 匹配a-z中任意字符
void test03(){
char buf[1024] = { 0 };
//跳过前面的数字
//先匹配第一个字符,判断字符是不是a-z中的字符,若是是匹配
//若是不是中止匹配
sscanf("abcdefg123456", "%[a-z]", buf);
printf("buf:%s\n", buf);
}

//4. 匹配aBc中的任何一个
void test04(){
char buf[1024] = { 0 };
//跳过前面的数字
//先匹配第一个字符是不是aBc中的一个,若是是,则匹配,若是不是则中止匹配
sscanf("abcdefg123456", "%[aBc]", buf);
printf("buf:%s\n", buf);
}

//5. 匹配非a的任意字符
void test05(){
char buf[1024] = { 0 };
//跳过前面的数字
//先匹配第一个字符是不是aBc中的一个,若是是,则匹配,若是不是则中止匹配
sscanf("bcdefag123456", "%[^a]", buf);
printf("buf:%s\n", buf);
}

//6. 匹配非a-z中的任意字符
void test06(){
char buf[1024] = { 0 };
//跳过前面的数字
//先匹配第一个字符是不是aBc中的一个,若是是,则匹配,若是不是则中止匹配
sscanf("123456ABCDbcdefag", "%[^a-z]", buf);
printf("buf:%s\n", buf);
}

4.5 一级指针易错点

4.5.1 越界

void test(){

char buf[3] = "abc";

printf("buf:%s\n",buf);

}

4.5.2 指针叠加会不断改变指针指向

void test(){

char *p = (char *)malloc(50);

char buf[] = "abcdef";

int n = strlen(buf);

int i = 0;

 

for (i = 0; i < n; i++)

{

*p = buf[i];

p++; //修改原指针指向

}

free(p);

}

4.5.3 返回局部变量地址

char *get_str()

{

char str[] = "abcdedsgads"; //栈区,

printf("[get_str]str = %s\n", str);

return str;

}

4.5.4 同一块内存释放屡次

void test(){

char *p = NULL;

p = (char *)malloc(50);

strcpy(p, "abcdef");

if (p != NULL)

{

//free()函数的功能只是告诉系统 p 指向的内存能够回收了

// 就是说,p 指向的内存使用权交还给系统

//可是,p的值仍是原来的值(野指针)p仍是指向原来的内存

free(p);

}

if (p != NULL)

{

free(p);

}

}

4.6 const使用

//const修饰变量
void test01(){
//1. const基本概念
const int i = 0;
//i = 100; //错误,只读变量初始化以后不能修改
//2. 定义const变量最好初始化
const int j;
//j = 100; //错误,不能再次赋值
//3. c语言的const是一个只读变量,并非一个常量,可经过指针间接修改
const int k = 10;
//k = 100; //错误,不可直接修改,咱们可经过指针间接修改
printf("k:%d\n", k);
int* p = &k;
*p = 100;
printf("k:%d\n", k);
}
//const 修饰指针
void test02(){
int a = 10;
int b = 20;
//const放在*号左侧 修饰p_a指针指向的内存空间不能修改,但可修改指针的指向
const int* p_a = &a;
//*p_a = 100; //不可修改指针指向的内存空间
p_a = &b; //可修改指针的指向
//const放在*号的右侧, 修饰指针的指向不能修改,可是可修改指针指向的内存空间
int* const p_b = &a;
//p_b = &b; //不可修改指针的指向
*p_b = 100; //可修改指针指向的内存空间
//指针的指向和指针指向的内存空间都不能修改
const int* const p_c = &a;
}
//const指针用法
struct Person{
char name[64];
int id;
int age;
int score;
};
//每次都对对象进行拷贝,效率低,应该用指针
void printPersonByValue(struct Person person){
printf("Name:%s\n", person.name);
printf("Name:%d\n", person.id);
printf("Name:%d\n", person.age);
printf("Name:%d\n", person.score);
}
//可是用指针会有反作用,可能会不当心修改原数据
void printPersonByPointer(const struct Person *person){
printf("Name:%s\n", person->name);
printf("Name:%d\n", person->id);
printf("Name:%d\n", person->age);
printf("Name:%d\n", person->score);
}
void test03(){
struct Person p = { "Obama", 1101, 23, 87 };
//printPersonByValue(p);
printPersonByPointer(&p);
}

5. 指针的指针(二级指针)

5.1 二级指针基本概念

  这里让咱们花点时间来看一个例子,揭开这个即将开始的序幕。考虑下面这些声明:

int a = 12;

int *b = &a;

它们以下图进行内存分配:

 

 

 

   假定咱们又有了第3个变量,名叫c,并用下面这条语句对它进行初始化:

c = &b;

它在内存中的大概模样大体以下:

 

  问题是:c的类型是什么?显然它是一个指针,但它所指向的是什么?变量b是一个“指向整型的指针”,因此任何指向b的类型必须是指向“指向整型的指针”的指针,更通俗地说,是一个指针的指针。

  它合法吗?是的!指针变量和其余变量同样,占据内存中某个特定的位置,因此用&操做符取得它的地址是合法的。

  那么这个变量的声明是怎样的声明的呢?

int **c = &b;

  那么这个**c如何理解呢*操做符具备从右想作的结合性,因此这个表达式至关于*(*c),咱们从里向外逐层求职*c访问c所指向的位置,咱们知道这是变量b.第二个间接访问操做符访问这个位置所指向的地址,也就是变量a.指针的指针并不难懂,只须要留心全部的箭头,若是表达式中出现了间接访问操做符,你就要随箭头访问它所指向的位置。

5.2 二级指针作形参输出特性

二级指针作参数的输出特性是指由被调函数分配内存。

//被调函数,由参数n肯定分配多少个元素内存
void allocate_space(int **arr,int n){
//堆上分配n个int类型元素内存
int *temp = (int *)malloc(sizeof(int)* n);
if (NULL == temp){
return;
}
//给内存初始化值
int *pTemp = temp;
for (int i = 0; i < n;i ++){
//temp[i] = i + 100;
*pTemp = i + 100;
pTemp++;
}
//指针间接赋值
*arr = temp;
}
//打印数组
void print_array(int *arr,int n){
for (int i = 0; i < n;i ++){
printf("%d ",arr[i]);
}
printf("\n");
}
//二级指针输出特性(由被调函数分配内存)
void test(){
int *arr = NULL;
int n = 10;
//给arr指针间接赋值
allocate_space(&arr,n);
//输出arr指向数组的内存
print_array(arr, n);
//释放arr所指向内存空间的值
if (arr != NULL){
free(arr);
arr = NULL;
}
}

5.3 二级指针作形参输入特性

 

  二级指针作形参输入特性是指由主调函数分配内存。

 

//打印数组
void print_array(int **arr,int n){
for (int i = 0; i < n;i ++){
printf("%d ",*(arr[i]));
}
printf("\n");
}
//二级指针输入特性(由主调函数分配内存)
void test(){
int a1 = 10;
int a2 = 20;
int a3 = 30;
int a4 = 40;
int a5 = 50;
int n = 5;
int** arr = (int **)malloc(sizeof(int *) * n);
arr[0] = &a1;
arr[1] = &a2;
arr[2] = &a3;
arr[3] = &a4;
arr[4] = &a5;
print_array(arr,n);
free(arr);
arr = NULL;
}

5.4 强化训练_画出内存模型图 

void mian()
{
//栈区指针数组
char *p1[] = { "aaaaa", "bbbbb", "ccccc" };
//堆区指针数组
char **p3 = (char **)malloc(3 * sizeof(char *)); //char *array[3];
int i = 0;
for (i = 0; i < 3; i++)
{
p3[i] = (char *)malloc(10 * sizeof(char)); //char buf[10]
sprintf(p3[i], "%d%d%d", i, i, i);
}
}

5.4 多级指针 

将堆区数组指针案例改成三级指针案例:

//分配内存
void allocate_memory(char*** p, int n){
if (n < 0){
return;
}
char** temp = (char**)malloc(sizeof(char*)* n);
if (temp == NULL){
return;
}
//分别给每个指针malloc分配内存
for (int i = 0; i < n; i++){
temp[i] = malloc(sizeof(char)* 30);
sprintf(temp[i], "%2d_hello world!", i + 1);
}
*p = temp;
}
//打印数组
void array_print(char** arr, int len){
for (int i = 0; i < len; i++){
printf("%s\n", arr[i]);
}
printf("----------------------\n");
}
//释放内存
void free_memory(char*** buf, int len){
if (buf == NULL){
return;
}
char** temp = *buf;
for (int i = 0; i < len; i++){
free(temp[i]);
temp[i] = NULL;
}
free(temp);
}
void test(){
int n = 10;
char** p = NULL;
allocate_memory(&p, n);
//打印数组
array_print(p, n);
//释放内存
free_memory(&p, n);
}

 


 

6. 位运算 

  能够使用C对变量中的个别位进行操做。您可能对人们想这样作的缘由感到奇怪。这种能力有时确实是必须的,或者至少是有用的。C提供位的逻辑运算符和移位运算符。在如下例子中,咱们将使用二进制计数法写出值,以便您能够了解对位发生的操做。在一个实际程序中,您能够使用通常的形式的整数变量或常量。例如不适用00011001的形式,而写为25或者031或者0x19.在咱们的例子中,咱们将使用8位数字,从左到右,每位的编号是7到0。

6.1位逻辑运算符

  4个位运算符用于整型数据,包括char.将这些位运算符成为位运算的缘由是它们对每位进行操做,而不影响左右两侧的位。请不要将这些运算符与常规的逻辑运算符(&& ||和!)相混淆,常规的位的逻辑运算符对整个值进行操做。

6.1.1 按位取反~

一元运算符~将每一个1变为0,将每一个0变为1,以下面的例子:

~(10011010)

01100101

  假设a是一个unsigned char,已赋值为2.在二进制中,2是00000010.因而-a的值为11111101或者253。请注意该运算符不会改变a的值,a仍为2。

 

unsigned char a = 2;   //00000010
unsigned char b = ~a;  //11111101
printf("ret = %d\n", a); //ret = 2
printf("ret = %d\n", b); //ret = 253

 

6.1.2 位与(AND: &

  二进制运算符&经过对两个操做数逐位进行比较产生一个新值。对于每一个位,只有两个操做数的对应位都是1时结果才为1。

   (10010011) 

 & (00111101) 

 = (00010001)

C也有一个组合的位与-赋值运算符&=。下面两个将产生相同的结果:

val &= 0377

val = val & 0377

6.1.3 位或(OR: |

二进制运算符|经过对两个操做数逐位进行比较产生一个新值。对于每一个位,若是其中任意操做数中对应的位为1,那么结果位就为1.

(10010011)

  | (00111101)

  = (10111111)

C也有组合位或-赋值运算符 |=

val |= 0377

val = val | 0377

6.1.4 位异或:

  二进制运算符^对两个操做数逐位进行比较。对于每一个位,若是操做数中的对应位有一个是1(但不是都是1),那么结果是1.若是都是0或者都是1,则结果位0.

(10010011)

  ^ (00111101)

  = (10101110)

C也有一个组合的位异或-赋值运算符: ^=

val ^= 0377

val = val ^ 0377

 6.1.5 用法

6.1.5.1 打开位

已知:10011010:

  1. 将位2打开

  flag |  10011010

(10011010)

|(00000100)

=(10011110)

 

  1. 将全部位打开。

  flag | ~flag

(10011010)

|(01100101)

=(11111111)

6.1.5.2 关闭位

  flag & ~flag

(10011010)

&(01100101)

=(00000000)

6.1.5.3 转置位

 转置(toggling)一个位表示若是该位打开,则关闭该位;若是该位关闭,则打开。您能够使用位异或运算符来转置。其思想是若是b是一个位(1或0),那么若是b为1则b^1为0,若是b为0,则1^b为1。不管b的值是0仍是1,0^b为b.

flag ^ 0xff

(10010011)

^(11111111)

=(01101100)

 6.1.5.4 交换两个数不须要临时变量

//a ^ b = temp;
//a ^ temp = b;
//b ^ temp = a
 (10010011)
^(00100110)
=(10110101)

 (10110101)
^(00100110)
  10010011

  int a = 10;
  int b = 30;

6.2 移位运算符 

  如今让咱们了解一下C的移位运算符。移位运算符将位向左或向右移动。一样,咱们仍将明确地使用二进制形式来讲明该机制的工做原理。

6.2.1 左移 <<

  左移运算符<<将其左侧操做数的值的每位向左移动,移动的位数由其右侧操做数指定。空出来的位用0填充,而且丢弃移出左侧操做数末端的位。在下面例子中,每位向左移动两个位置。

(10001010) << 2

(00101000)

该操做将产生一个新位置,可是不改变其操做数。

<< 1 = 2;

<< 1 = 4;

<< 1 = 8;

<< 2 = 32

左移一位至关于原值*2.

6.2.2 右移 >>

右移运算符>>将其左侧的操做数的值每位向右移动,移动的位数由其右侧的操做数指定。丢弃移出左侧操做数有段的位。对于unsigned类型,使用0填充左端空出的位。对于有符号类型,结果依赖于机器。空出的位可能用0填充,或者使用符号(最左端)位的副本填充。

//有符号值

(10001010) >> 2

(00100010)     //在某些系统上的结果值

(10001010) >> 2

(11100010)     //在另外一些系统上的解雇

//无符号值

(10001010) >> 2

(00100010)    //全部系统上的结果值

6.2.3 用法:移位运算符

  移位运算符可以提供快捷、高效(依赖于硬件)对2的幂的乘法和除法。

number << n

number乘以2的n次幂

number >> n

若是number非负,则用number除以2的n次幂

 


 

6. 多维数组

6.1 一维数组

  • 元素类型角度:数组是相同类型的变量的有序集合

  • 内存角度:连续的一大片内存空间

 

 

 

   在讨论多维数组以前,咱们还须要学习不少关于一维数组的知识。首先让咱们学习一个概念。

6.1.1 数组名

考虑下面这些声明:

int a;

int b[10];

  咱们把a称做标量,由于它是个单一的值,这个变量是的类型是一个整数。咱们把b称做数组,由于它是一些值的集合。下标和数名一块儿使用,用于标识该集合中某个特定的值。例如,b[0]表示数组b的第1个值,b[4]表示第5个值。每一个值都是一个特定的标量。

  那么问题是b的类型是什么?它所表示的又是什么?一个合乎逻辑的答案是它表示整个数组,但事实并不是如此。在C中,在几乎全部数组名的表达式中,数组名的值是一个指针常量,也就是数组第一个元素的地址。它的类型取决于数组元素的类型:若是他们是int类型,那么数组名的类型就是“指向int的常量指针”;若是它们是其余类型,那么数组名的类型也就是“指向其余类型的常量指针”。

请问:指针和数组是等价的吗?

  答案是否认的。数组名在表达式中使用的时候,编译器才会产生一个指针常量。那么数组在什么状况下不能做为指针常量呢?在如下两种场景下:

n 当数组名做为sizeof操做符的操做数的时候,此时sizeof返回的是整个数组的长度,而不是指针数组指针的长度。

n 当数组名做为&操做符的操做数的时候,此时返回的是一个指向数组的指针,而不是指向某个数组元素的指针常量。

int arr[10];
//arr = NULL; //arr做为指针常量,不可修改
int *p = arr; //此时arr做为指针常量来使用
printf("sizeof(arr):%d\n", sizeof(arr)); //此时sizeof结果为整个数组的长度
printf("&arr type is %s\n", typeid(&arr).name()); //int(*)[10]而不是int*

6.1.2 下标引用 

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

  首先,咱们说数组在表达式中是一个指向整型的指针,因此此表达式表示arr指针向后移动了3个元素的长度。而后经过间接访问操做符从这个新地址开始获取这个位置的值。这个和下标的引用的执行过程彻底相同。因此以下表达式是等同的:*(arr + 3) ,这个表达式是什么意思呢?

*(arr + 3)
arr[3]

问题1:数组下标能否为负值?

问题2:请阅读以下代码,说出结果:

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

那么是用下标仍是指针来操做数组呢?对于大部分人而言,下标的可读性会强一些。 

6.1.3 数组和指针

指针和数组并非相等的。为了说明这个概念,请考虑下面两个声明:

int a[10];

int *b;

  声明一个数组时,编译器根据声明所指定的元素数量为数组分配内存空间,而后再建立数组名,指向这段空间的起始位置。声明一个指针变量的时候,编译器只为指针自己分配内存空间,并不为任何整型值分配内存空间,指针并未初始化指向任何现有的内存空间。 

  所以,表达式*a是彻底合法的,可是表达式*b倒是非法的。*b将访问内存中一个不肯定的位置,将会致使程序终止。另外一方面b++能够经过编译,a++却不行,由于a是一个常量值。

6.1.4 做为函数参数的数组名

  当一个数组名做为一个参数传递给一个函数的时候发生什么状况呢?咱们如今知道数组名其实就是一个指向数组第1个元素的指针,因此很明白此时传递给函数的是一份指针的拷贝。因此函数的形参其实是一个指针。可是为了使程序员新手容易上手一些,编译器也接受数组形式的函数形参。所以下面两种函数原型是相等的:

int print_array(int *arr);

int print_array(int arr[]);

  如今咱们清楚了,为何一维数组中无须写明它的元素数目了,由于形参只是一个指针,并不须要为数组参数分配内存。另外一方面,这种方式使得函数没法知道数组的长度。若是函数须要知道数组的长度,它必须显式传递一个长度参数给函数。咱们能够使用任何一种声明,但哪个更准确一些呢?答案是指针。由于实参其实是个指针,而不是数组。一样sizeof arr值是指针的长度,而不是数组的长度。

6.2 多维数组

若是某个数组的维数不止1个,它就被称为多维数组。接下来的案例讲解以二维数组举例。

void test01(){
//二维数组初始化
int arr1[3][3] = {
{ 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 }
};
int arr2[3][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int arr3[][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
//打印二维数组
for (int i = 0; i < 3; i++){
for (int j = 0; j < 3; j ++){
printf("%d ",arr1[i][j]);
}
printf("\n");
}
}

6.2.1 数组名 

一维数组名的值是一个指针常量,它的类型是“指向元素类型的指针”,它指向数组的第1个元素。多维数组也是同理,多维数组的数组名也是指向第一个元素,只不过第一个元素是一个数组。例如:

int arr[3][10]

6.2.2 指向数组的指针(数组指针)

  能够理解为这是一个一维数组,包含了3个元素,只是每一个元素刚好是包含了10个元素的数组。arr就表示指向它的第1个元素的指针,因此arr是一个指向了包含了10个整型元素的数组的指针。

  数组指针,它是指针,指向数组的指针。

  数组的类型由元素类型数组大小共同决定:int array[5]  的类型为  int[5];C语言可经过typedef定义一个数组类型:

  定义数组指针有一下三种方式:

//方式一
void test01(){
//先定义数组类型,再用数组类型定义数组指针
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
//有typedef是定义类型,没有则是定义变量,下面代码定义了一个数组类型ArrayType
typedef int(ArrayType)[10];
//int ArrayType[10]; //定义一个数组,数组名为ArrayType
ArrayType myarr; //等价于 int myarr[10];
ArrayType* pArr = &arr; //定义了一个数组指针pArr,而且指针指向数组arr
for (int i = 0; i < 10;i++){
printf("%d ",(*pArr)[i]);
}
printf("\n");
}
 
//方式二
void test02(){
int arr[10];
//定义数组指针类型
typedef int(*ArrayType)[10];
ArrayType pArr = &arr; //定义了一个数组指针pArr,而且指针指向数组arr
for (int i = 0; i < 10; i++){
(*pArr)[i] = i + 1;
}
for (int i = 0; i < 10; i++){
printf("%d ", (*pArr)[i]);
}
printf("\n");
}

//方式三
void test03(){
int arr[10];
int(*pArr)[10] = &arr;
for (int i = 0; i < 10; i++){
(*pArr)[i] = i + 1;
}
for (int i = 0; i < 10; i++){
printf("%d ", (*pArr)[i]);
}
printf("\n");
}

6.2.3 指针数组(元素为指针)

 

6.3.1 栈区指针数组

 

//数组作函数函数,退化为指针
void array_sort(char** arr,int len){
for (int i = 0; i < len; i++){
for (int j = len - 1; j > i; j --){
//比较两个字符串
if (strcmp(arr[j-1],arr[j]) > 0){
char* temp = arr[j - 1];
arr[j - 1] = arr[j];
arr[j] = temp;
}
}
}
}
//打印数组
void array_print(char** arr,int len){
for (int i = 0; i < len;i++){
printf("%s\n",arr[i]);
}
printf("----------------------\n");
}
void test(){
//主调函数分配内存
//指针数组
char* p[] = { "bbb", "aaa", "ccc", "eee", "ddd"};
//char** p = { "aaa", "bbb", "ccc", "ddd", "eee" }; //错误
int len = sizeof(p) / sizeof(char*);
//打印数组
array_print(p, len);
//对字符串进行排序
array_sort(p, len);
//打印数组
array_print(p, len);
} 

 6.3.2 堆区指针数组

//分配内存
char** allocate_memory(int n){
if (n < 0 ){
return NULL;
}
char** temp = (char**)malloc(sizeof(char*) * n);
if (temp == NULL){
return NULL;
}
//分别给每个指针malloc分配内存
for (int i = 0; i < n; i ++){
temp[i] = malloc(sizeof(char)* 30);
sprintf(temp[i], "%2d_hello world!", i + 1);
}
return temp;
}
//打印数组
void array_print(char** arr,int len){
for (int i = 0; i < len;i++){
printf("%s\n",arr[i]);
}
printf("----------------------\n");
}
//释放内存
void free_memory(char** buf,int len){
if (buf == NULL){
return;
}
for (int i = 0; i < len; i ++){
free(buf[i]);
buf[i] = NULL;
}
free(buf);
}
void test(){
int n = 10;
char** p = allocate_memory(n);
//打印数组
array_print(p, n);
//释放内存
free_memory(p, n);
}

6.2.4二维数组三种参数形式

6.2.4.1 二维数组的线性存储特性

void PrintArray(int* arr, int len){
for (int i = 0; i < len; i++){
printf("%d ", arr[i]);
}
printf("\n");
}
//二维数组的线性存储
void test(){
int arr[][3] = {
{ 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 }
};
int arr2[][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int len = sizeof(arr2) / sizeof(int);
//如何证实二维数组是线性的?
//经过将数组首地址指针转成Int*类型,那么步长就变成了4,就能够遍历整个数组
int* p = (int*)arr;
for (int i = 0; i < len; i++){
printf("%d ", p[i]);
}
printf("\n");
PrintArray((int*)arr, len);
PrintArray((int*)arr2, len);
}

6.2.4.2 二维数组的3种形式参数 

//二维数组的第一种形式
void PrintArray01(int arr[3][3]){
for (int i = 0; i < 3; i++){
for (int j = 0; j < 3; j++){
printf("arr[%d][%d]:%d\n", i, j, arr[i][j]);
}
}
}
//二维数组的第二种形式
void PrintArray02(int arr[][3]){
for (int i = 0; i < 3; i++){
for (int j = 0; j < 3; j++){
printf("arr[%d][%d]:%d\n", i, j, arr[i][j]);
}
}
}
//二维数组的第二种形式
void PrintArray03(int(*arr)[3]){
for (int i = 0; i < 3; i++){
for (int j = 0; j < 3; j++){
printf("arr[%d][%d]:%d\n", i, j, arr[i][j]);
}
}
}
void test(){
int arr[][3] = { 
{ 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 }
};
PrintArray01(arr);
PrintArray02(arr);
PrintArray03(arr);
}

6.3总结

6.3.1 编程提示

  • 源代码的可读性几乎老是比程序的运行时效率更为重要

  • 只要有可能,函数的指针形参都应该声明为const

  • 在多维数组的初始值列表中使用完整的多层花括号提供可读性

6.3.2 内容总结

  在绝大多数表达式中,数组名的值是指向数组第1个元素的指针。这个规则只有两个例外,sizeof和对数组名&。

  指针和数组并不相等。当咱们声明一个数组的时候,同时也分配了内存。可是声明指针的时候,只分配容纳指针自己的空间。

  当数组名做为函数参数时,实际传递给函数的是一个指向数组第1个元素的指针。

  咱们不单能够建立指向普通变量的指针,也可建立指向数组的指针。


 

7. 结构体

7.1 结构体基础知识

7.1.1 结构体类型的定义

struct Person{
char name[64];
int age;
};
typedef struct _PERSON{
char name[64];
int age;
}Person;

  注意:定义结构体类型时不要直接给成员赋值,结构体只是一个类型,编译器尚未为其分配空间,只有根据其类型定义变量时,才分配空间,有空间后才能赋值。 

7.1.2 结构体变量的定义

struct Person{
char name[64];
int age;
}p1; //定义类型同时定义变量
struct{
char name[64];
int age;
}p2; //定义类型同时定义变量
struct Person p3; //经过类型直接定义 

7.1.3 结构体变量的初始化

struct Person{
char name[64];
int age;
}p1 = {"john",10}; //定义类型同时初始化变量
struct{
char name[64];
int age;
}p2 = {"Obama",30}; //定义类型同时初始化变量
struct Person p3 = {"Edward",33}; //经过类型直接定义

7.1.4 结构体成员的使用

struct Person{
char name[64];
int age;
};
void test(){
//在栈上分配空间
struct Person p1;
strcpy(p1.name, "John");
p1.age = 30;
//若是是普通变量,经过点运算符操做结构体成员
printf("Name:%s Age:%d\n", p1.name, p1.age);
//在堆上分配空间
struct Person* p2 = (struct Person*)malloc(sizeof(struct Person));
strcpy(p2->name, "Obama");
p2->age = 33;
//若是是指针变量,经过->操做结构体成员
printf("Name:%s Age:%d\n", p2->name, p2->age);
}

7.1.5 结构体赋值 

7.1.5.1 赋值基本概念

  相同的两个结构体变量能够相互赋值,把一个结构体变量的值拷贝给另外一个结构体,这两个变量仍是两个独立的变量。

struct Person{
char name[64];
int age;
};
void test(){
//在栈上分配空间
struct Person p1 = { "John" , 30};
struct Person p2 = { "Obama", 33 };
printf("Name:%s Age:%d\n", p1.name, p1.age);
printf("Name:%s Age:%d\n", p2.name, p2.age);
//将p2的值赋值给p1
p1 = p2;
printf("Name:%s Age:%d\n", p1.name, p1.age);
printf("Name:%s Age:%d\n", p2.name, p2.age);
}

  

 


 7.1.5.1 深拷贝和浅拷贝

 

//一个老师有N个学生
typedef struct _TEACHER{
char* name;
}Teacher;
void test(){
Teacher t1;
t1.name = malloc(64);
strcpy(t1.name , "John");
Teacher t2;
t2 = t1;
//对手动开辟的内存,须要手动拷贝
t2.name = malloc(64);
strcpy(t2.name, t1.name);
if (t1.name != NULL){
free(t1.name);
t1.name = NULL;
}
if (t2.name != NULL)
  { free(t2.name); t1.name = NULL;   } }

 

7.1.6 结构体数组

struct Person{
char name[64];
int age;
};
void test(){
//在栈上分配空间
struct Person p1[3] = {
{ "John", 30 },
{ "Obama", 33 },
{ "Edward", 25}
};
struct Person p2[3] = { "John", 30, "Obama", 33, "Edward", 25 };
for (int i = 0; i < 3;i ++){
printf("Name:%s Age:%d\n",p1[i].name,p1[i].age);
}
printf("-----------------\n");
for (int i = 0; i < 3; i++){
printf("Name:%s Age:%d\n", p2[i].name, p2[i].age);
}
printf("-----------------\n");

//在堆上分配结构体数组
struct Person* p3 = (struct Person*)malloc(sizeof(struct Person) * 3);
for (int i = 0; i < 3;i++){
sprintf(p3[i].name, "Name_%d", i + 1);
p3[i].age = 20 + i;
}
for (int i = 0; i < 3; i++){
printf("Name:%s Age:%d\n", p3[i].name, p3[i].age);
}
}

7.2 结构体嵌套指针 

7.2.1 结构体嵌套一级指针
struct Person{
char* name;
int age;
};
void allocate_memory(struct Person** person){
if (person == NULL){
return;
}
struct Person* temp = (struct Person*)malloc(sizeof(struct Person));
if (temp == NULL){
return;
}
//给name指针分配内存
temp->name = (char*)malloc(sizeof(char)* 64);
strcpy(temp->name, "John");
temp->age = 100;
*person = temp;
}
void print_person(struct Person* person){
printf("Name:%s Age:%d\n",person->name,person->age);
}
void free_memory(struct Person** person){
if (person == NULL){
return;
}
struct Person* temp = *person;
if (temp->name != NULL){
free(temp->name);
temp->name = NULL;
}
free(temp);
}
void test(){
struct Person* p = NULL;
allocate_memory(&p);
print_person(p);
free_memory(&p);
}

7.2.2 结构体嵌套二级指针

//一个老师有N个学生
typedef struct _TEACHER{
char name[64];
char** students;
}Teacher;
void create_teacher(Teacher** teacher,int n,int m){
if (teacher == NULL){
return;
}
//建立老师数组
Teacher* teachers = (Teacher*)malloc(sizeof(Teacher)* n);
if (teachers == NULL){
return;
}
//给每个老师分配学生
int num = 0;
for (int i = 0; i < n; i ++){
sprintf(teachers[i].name, "老师_%d", i + 1);
teachers[i].students = (char**)malloc(sizeof(char*) * m);
for (int j = 0; j < m;j++){
teachers[i].students[j] = malloc(64);
sprintf(teachers[i].students[j], "学生_%d", num + 1);
num++;
}
}
*teacher = teachers;
}
void print_teacher(Teacher* teacher,int n,int m){
for (int i = 0; i < n; i ++){
printf("%s:\n", teacher[i].name);
for (int j = 0; j < m;j++){
printf("  %s",teacher[i].students[j]);
}
printf("\n");
}
}
void free_memory(Teacher** teacher,int n,int m){
if (teacher == NULL){
return;
}
Teacher* temp = *teacher;
for (int i = 0; i < n; i ++){
for (int j = 0; j < m;j ++){
free(temp[i].students[j]);
temp[i].students[j] = NULL;
}
free(temp[i].students);
temp[i].students = NULL;
}
free(temp);
}
void test(){
Teacher* p = NULL;
create_teacher(&p,2,3);
print_teacher(p, 2, 3);
free_memory(&p,2,3);
}

7.3 结构体成员偏移量

//一旦结构体定义下来,则结构体中的成员内存布局就定下了
typedef struct Teacher
{
char a;  
int b;      
int c;        
} Teacher;
void test(){
Teacher  t1;
Teacher*p = NULL;
p = &t1;
int offsize1 = (int)&(p->b) - (int)p;  //age 相对于结构体 Teacher的偏移量
int offsize2 = (int)&(((Teacher *)0)->b);//绝对0地址 age的偏移量
int offsize3 = offsetof(Teacher, b);
printf("offsize1:%d \n", offsize1);
printf("offsize2:%d \n", offsize2);
printf("offsize3:%d \n", offsize3);
}

7.4 结构体字节对齐 

  在用sizeof运算符求算某结构体所占空间时,并非简单地将结构体中全部元素各自占的空间相加,这里涉及到内存字节对齐的问题。

  从理论上讲,对于任何变量的访问均可以从任何地址开始访问,可是事实上不是如此,实际上访问特定类型的变量只能在特定的地址访问,这就须要各个变量在空间上按必定的规则排列, 而不是简单地顺序排列,这就是内存对齐。

7.4.1 内存对齐

7.4.1.1 内存对齐缘由

  咱们知道内存的最小单元是一个字节,当cpu从内存中读取数据的时候,是一个一个字节读取,因此内存对咱们应该是入下图这样:

 

 

   可是实际上cpu将内存当成多个块,每次从内存中读取一个块,这个块的大小多是二、四、八、16等,

  那么下面,咱们来分析下非内存对齐和内存对齐的优缺点在哪?

  内存对齐是操做系统为了提升访问内存的策略。操做系统在访问内存的时候,每次读取必定长度(这个长度是操做系统默认的对齐数,或者默认对齐数的整数倍)。若是没有对齐,为了访问一个变量可能产生二次访问。

至此你们应该可以简单明白,为何要简单内存对齐?

提升存取数据的速度。好比有的平台每次都是从偶地址处读取数据,对于一个int型的变量,若从偶地址单元处存放,则只需一个读取周期便可读取该变量;可是若从奇地址单元处存放,则须要2个读取周期读取该变量。

某些平台只能在特定的地址处访问特定类型的数据,不然抛出硬件异常给操做系统。

7.4.1.2 如何内存对齐

  • 对于标准数据类型,它的地址只要是它的长度的整数倍。

  • 对于非标准数据类型,好比结构体,要遵循一下对齐原则:

1. 数组成员对齐规则。第一个数组成员应该放在offset为0的地方,之后每一个数组成员应该放在offset为min(当前成员的大小,#pargama pack(n))整数倍的地方开始(好比int在32位机器为4字节,#pargama pack(2),那么从2的倍数地方开始存储)。
2. 结构体总的大小,也就是sizeof的结果,必须是min(结构体内部最大成员,#pargama pack(n))的整数倍,不足要补齐。
3. 结构体作为成员的对齐规则。若是一个结构体B里嵌套另外一个结构体A,仍是以最大成员类型的大小对齐,可是结构体A的起点为A内部最大成员的整数倍的地方。(struct B里存有struct A,A里有char,int,double等成员,那A应该从8的整数倍开始存储。),结构体A中的成员的对齐规则仍知足原则一、原则2。

手动设置对齐模数

#pragma pack(show)
显示当前packing alignment的字节数,以warning message的形式被显示。

#pragma pack(push)
将当前指定的packing alignment数组进行压栈操做,这里的栈是the internal compiler stack,同事设置当前的packing alignment为n;若是n没有指定,则将当前的packing alignment数组压栈。

#pragma pack(pop)
从internal compiler stack中删除最顶端的reaord; 若是没有指定n,则当前栈顶record即为新的packing alignement数值;若是指定了n,则n成为新的packing alignment值

#pragma pack(n)
指定packing的数值,以字节为单位,缺省数值是8,合法的数值分别是1,2,4,8,16

7.4.2 内存对齐案例 

#pragma pack(4)
typedef struct _STUDENT{
int a;
char b;
double c;
float d;
}Student;
typedef struct _STUDENT2{
char a;
Student b; 
double c;
}Student2;
void test01(){
//Student
//a从偏移量0位置开始存储
//b从4位置开始存储
//c从8位置开始存储
//d从12位置开存储
//因此Student内部对齐以后的大小为20 ,总体对齐,总体为最大类型的整数倍 也就是8的整数倍 为24
printf("sizeof Student:%d\n",sizeof(Student));
//Student2
//a从偏移量为0位置开始 8
//b从偏移量为Student内部最大成员整数倍开始,也就是8开始 24
//c从8的整数倍地方开始,也就是32开始
//因此结构体Sutdnet2内部对齐以后的大小为:40 , 因为结构体中最大成员为8,必须为8的整数倍 因此大小为40
printf("sizeof Student2:%d\n", sizeof(Student2));
}

8. 文件操做 

  文件在今天的计算机系统中做用是很重要的。文件用来存放程序、文档、数据、表格、图片和其余不少种类的信息。做为一名程序员,您必须编程来建立、写入和读取文件。编写程序从文件读取信息或者将结果写入文件是一种常常性的需求。C提供了强大的和文件进行通讯的方法。使用这种方法咱们能够在程序中打开文件,而后使用专门的I/O函数读取文件或者写入文件。

8.1 文件相关概念

8.1.1 文件的概念

  一个文件一般就是磁盘上一段命名的存储区。可是对于操做系统来讲,文件就会更复杂一些。例如,一个大文件能够存储在一些分散的区段中,或者还会包含一些操做系统能够肯定其文件类型的附加数据,可是这些是操做系统,而不是咱们程序员所要关心的事情。咱们应该考虑如何在C程序中处理文件。

8.1.2 流的概念

  流是一个动态的概念,能够将一个字节形象地比喻成一滴水,字节在设备、文件和程序之间的传输就是流,相似于水在管道中的传输,能够看出,流是对输入输出源的一种抽象,也是对传输信息的一种抽象。

  C语言中,I/O操做能够简单地看做是从程序移进或移出字节,这种搬运的过程便称为流(stream)。程序只须要关心是否正确地输出了字节数据,以及是否正确地输入了要读取字节数据,特定I/O设备的细节对程序员是隐藏的。

8.1.2.1 文本流

  文本流,也就是咱们常说的以文本模式读取文件。文本流的有些特性在不一样的系统中可能不一样。其中之一就是文本行的最大长度。标准规定至少容许254个字符。另外一个可能不一样的特性是文本行的结束方式。例如在Windows系统中,文本文件约定以一个回车符和一个换行符结尾。可是在Linux下只使用一个换行符结尾。

  标准C把文本定义为零个或者多个字符,后面跟一个表示结束的换行符(\n).对于那些文本行的外在表现形式与这个定义不一样的系统上,库函数负责外部形式和内部形式之间的翻译。例如,Windows系统中,在输出时,文本的换行符被写成一对回车/换行符。在输入时,文本中的回车符被丢弃。这种没必要考虑文本的外部形势而操纵文本的能力简化了可移植程序的建立。

8.1.2.1 二进制流

  二进制流中的字节将彻底根据程序编写它们的形式写入到文件中,并且彻底根据它们从文件或设备读取的形式读入到程序中。它们并未作任何改变。这种类型的流适用于非文本数据,可是若是你不但愿I/O函数修改文本文件的行末字符,也能够把它们用于文本文件。

  c语言在处理这两种文件的时候并不区分,都当作是字符流,按字节进行处理。

咱们程序中,常常看到的文本方式打开文件和二进制方式打开文件仅仅体如今换行符的处理上。

  好比说,在widows下,文件的换行符是\r\n,而在Linux下换行符则是\n.

当对文件使用文本方式打开的时候,读写的windows文件中的换行符\r\n会被替换成\n读到内存中,当在windows下写入文件的时候,\n被替换成\r\n再写入文件。若是使用二进制方式打开文件,则不进行\r\n和\n之间的转换。 那么因为Linux下的换行符就是\n,因此文本文件方式和二进制方式无区别。

8.2 文件的操做

8.2.1 文件流总览

  标准库函数是的咱们在C程序中执行与文件相关的I/O任务很是方便。下面是关于文件I/O的通常概况。

程序为同时处于活动状态的每一个文件声明一个指针变量,其类型为FILE*。这个指针指向这个FILE结构,当它处于活动状态时由流使用。
流经过fopen函数打开。为了打开一个流,咱们必须指定须要访问的文件或设备以及他们的访问方式(读、写、或者读写)。Fopen和操做系统验证文件或者设备是否存在并初始化FILE。
根据须要对文件进行读写操做。
最后调用fclose函数关闭流。关闭一个流能够防止与它相关的文件被再次访问,保证任何存储于缓冲区中的数据被正确写入到文件中,而且释放FILE结构。

  标准I/O更为简单,由于它们并不须要打开或者关闭。 

  I/O函数以三种基本的形式处理数据:单个字符文本行二进制数据。对于每种形式都有一组特定的函数对它们进行处理。

输入/输出函数家族

家族名

目的

可用于全部流

只用于stdin和stdout

getchar

字符输入

fgetc、getc

getchar

putchar

字符输出

fputc、putc

putchar

gets

文本行输入

fgets

gets

puts

文本行输出

fputs

puts

scanf

格式化输入

fscanf

scanf

printf

格式化输出

fprintf

printf

8.2.2 文件指针

  咱们知道,文件是由操做系统管理的单元。当咱们想操做一个文件的时候,让操做系统帮咱们打开文件,操做系统把咱们指定要打开文件的信息保存起来,而且返回给咱们一个指针指向文件的信息。文件指针也能够理解为代指打开的文件。这个指针的类型为FILE类型。该类型定义在stdio.h头文件中。经过文件指针,咱们就能够对文件进行各类操做。

  对于每个ANSI C程序,运行时系统必须提供至少三个流-标准输入(stdin)、标准输出(stdout)、标准错误(stderr),它们都是一个指向FILE结构的指针。标准输入是缺省状况下的输入来源,标准输出时缺省状况下的输出设置。具体缺省值因编译器而异,一般标准输入为键盘设备、标准输出为终端或者屏幕。

  ANSI C并未规定FILE的成员,不一样编译器可能有不一样的定义。VS下FILE信息以下:

struct _iobuf { 
        char  *_ptr;         //文件输入的下一个位置
        int   _cnt;          //剩余多少字符未被读取
        char  *_base;        //指基础位置(应该是文件的其始位置)
        int   _flag;         //文件标志
        int   _file;         //文件的有效性验证
        int   _charbuf;      //检查缓冲区情况,若是无缓冲区则不读取
        int   _bufsiz;       //文件的大小
        char  *_tmpfname;    //临时文件名
}; 
typedef struct _iobuf FILE;

8.2.3 文件缓冲区 

  • 文件缓冲区

  ANSI C标准采用“缓冲文件系统”处理数据文件 所谓缓冲文件系统是指系统自动地在内存区为程序中每个正在使用的文件开辟一个文件缓冲区从内存向磁盘输出数据必须先送到内存中的缓冲区,装满缓冲区后才一块儿送到磁盘去 若是从磁盘向计算机读入数据,则一次从磁盘文件将一批数据输入到内存缓冲区(充满缓冲 ),而后再从缓冲区逐个地将数据送到程序数据区(给程序变量) 。

那么文件缓冲区有什么做用呢?

如咱们从磁盘里取信息,咱们先把读出的数据放在缓冲区,计算机再直接从缓冲区中取数据,等缓冲区的数据取完后再去磁盘中读取,这样就能够减小磁盘的读写次数,再加上计算机对缓冲区的操做大大快于对磁盘的操做,故应用缓冲区可大大提升计算机的运行速度。

 

8.2.4 文件打开关闭

8.2.4.1 文件打开(fopen)

  文件的打开操做表示将给用户指定的文件在内存分配一个FILE结构区,并将该结构的指针返回给用户程序,之后用户程序就可用此FILE指针来实现对指定文件的存取操做了。当使用打开函数时,必须给出文件名、文件操做方式(读、写或读写)。

FILE * fopen(const char * filename, const char * mode);
功能:打开文件
参数:
filename:须要打开的文件名,根据须要加上路径
mode:打开文件的权限设置
返回值:
成功:文件指针
失败:NULL

方式

含义

“r”

打开,只读,文件必须已经存在。

“w”

只写,若是文件不存在则建立,若是文件已存在则把文件长度截断(Truncate)为0字节。再从新写,也就是替换掉原来的文件内容文件指针指到头。

“a”

只能在文件末尾追加数据,若是文件不存在则建立

“rb”

打开一个二进制文件,只读

“wb”

打开一个二进制文件,只写

“ab"

打开一个二进制文件,追加

“r+”

容许读和写,文件必须已存在

“w+”

容许读和写,若是文件不存在则建立,若是文件已存在则把文件长度截断为0字节再从新写 。

“a+”

容许读和追加数据,若是文件不存在则建立

“rb+”

以读/写方式打开一个二进制文件

“wb+”

以读/写方式创建一个新的二进制文件

“ab+”

以读/写方式打开一个二进制文件进行追加

 示例代码:

void test(){
FILE *fp = NULL;
// "\\"这样的路径形式,只能在windows使用
// "/"这样的路径形式,windows和linux平台下均可用,建议使用这种
// 路径能够是相对路径,也但是绝对路径
fp = fopen("../test", "w");
//fp = fopen("..\\test", "w");
if (fp == NULL) //返回空,说明打开失败
{
//perror()是标准出错打印函数,能打印调用库函数出错缘由
perror("open");
return -1;
}
}

应该检查fopen的返回值!如何函数失败,它会返回一个NULL值。若是程序不检查错误,这个NULL指针就会传给后续的I/O函数。它们将对这个指针执行间接访问,并将失败.

 8.2.4.2 文件关闭(fclose)

  文件操做完成后,若是程序没有结束,必需要用fclose()函数进行关闭,这是由于对打开的文件进行写入时,若文件缓冲区的空间未被写入的内容填满,这些内容不会写到打开的文件中。只有对打开的文件进行关闭操做时,停留在文件缓冲区的内容才能写到该文件中去,从而使文件完整。再者一旦关闭了文件,该文件对应的FILE结构将被释放,从而使关闭的文件获得保护,由于这时对该文件的存取操做将不会进行。文件的关闭也意味着释放了该文件的缓冲区。

int fclose(FILE * stream);
功能:关闭先前fopen()打开的文件。此动做让缓冲区的数据写入文件中,并释放系统所提供的文件资源。
参数:
stream:文件指针
返回值:
成功:0
失败:-1

  它表示该函数将关闭FILE指针对应的文件,并返回一个整数值。若成功地关闭了文件,则返回一个0值,不然返回一个非0值. 

8.2.4 文件读写函数回顾

按照字符读写文件:fgetc(), fputc()

按照行读写文件:fputs(), fgets()

按照块读写文件:fread(), fwirte()

按照格式化读写文件:fprintf(), fscanf()

按照随机位置读写文件:fseek(), ftell(), rewind()

 

 


 8.2.4.1 字符读写函数回顾

int fputc(int ch, FILE * stream);
功能:将ch转换为unsigned char后写入stream指定的文件中
参数:
ch:须要写入文件的字符
stream:文件指针
返回值:
成功:成功写入文件的字符
失败:返回-1
int fgetc(FILE * stream);
功能:从stream指定的文件中读取一个字符
参数:
stream:文件指针
返回值:
成功:返回读取到的字符
失败:-1
int feof(FILE * stream);
功能:检测是否读取到了文件结尾
参数:
stream:文件指针
返回值:
非0值:已经到文件结尾
0:没有到文件结尾
void test(){
//写文件
FILE* fp_write= NULL;
//写方式打开文件
fp_write = fopen("./mydata.txt", "w+");
if (fp_write == NULL){
return;
}
char buf[] = "this is a test for pfutc!";
for (int i = 0; i < strlen(buf);i++){
fputc(buf[i], fp_write);
}
fclose(fp_write);
//读文件
FILE* fp_read = NULL;
fp_read = fopen("./mydata.txt", "r");
if (fp_read == NULL){
return;
}
#if 0
//判断文件结尾 注意:多输出一个空格
while (!feof(fp_read)){
printf("%c",fgetc(fp_read));
}
#else
char ch;
while ((ch = fgetc(fp_read)) != EOF){
printf("%c", ch);
}
#endif
}

  将把流指针fp指向的文件中的一个字符读出,并赋给ch,当执行fgetc()函数时,若当时文件指针指到文件尾,即遇到文件结束标志EOF(其对应值为-1),该函数返回一个 -1 给ch,在程序中经常使用检查该函数返回值是否为 -1 来判断是否已读到文件尾,从而决定是否继续。

8.2.4.2 行读写函数回顾
int fputs(const char * str, FILE * stream);
功能:将str所指定的字符串写入到stream指定的文件中, 字符串结束符 '\0'  不写入文件。
参数:
str:字符串
stream:文件指针
返回值:
成功:0
失败:-1
char * fgets(char * str, int size, FILE * stream);
功能:从stream指定的文件内读入字符,保存到str所指定的内存空间,直到出现换行字符、读到文件结尾或是已读了size - 1个字符为止,最后会自动加上字符 '\0' 做为字符串结束。
参数:
str:字符串
size:指定最大读取字符串的长度(size - 1)
stream:文件指针
返回值:
成功:成功读取的字符串
读到文件尾或出错: NULL

 

void test(){
//写文件
FILE* fp_write= NULL;
//写方式打开文件
fp_write = fopen("./mydata.txt", "w+");
if (fp_write == NULL){
perror("fopen:");
return;
}
char* buf[] = {
"01 this is a test for pfutc!\n",
"02 this is a test for pfutc!\n",
"03 this is a test for pfutc!\n",
"04 this is a test for pfutc!\n",
};
for (int i = 0; i < 4; i ++){
fputs(buf[i], fp_write);
}
fclose(fp_write);
//读文件
FILE* fp_read = NULL;
fp_read = fopen("./mydata.txt", "r");
if (fp_read == NULL){
perror("fopen:");
return;
}
//判断文件结尾
while (!feof(fp_read)){
char temp[1024] = { 0 };
fgets(temp, 1024, fp_read);
printf("%s",temp);
}
fclose(fp_read);
}

8.2.4.3 块读写函数回顾

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
功能:以数据块的方式给文件写入内容
参数:
ptr:准备写入文件数据的地址
size: size_t 为 unsigned int类型,此参数指定写入文件内容的块数据大小
nmemb:写入文件的块数,写入文件数据总大小为:size * nmemb
stream:已经打开的文件指针
返回值:
成功:实际成功写入文件数据的块数,此值和nmemb相等
失败:0

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
功能:以数据块的方式从文件中读取内容
参数:
ptr:存放读取出来数据的内存空间
size: size_t 为 unsigned int类型,此参数指定读取文件内容的块数据大小
nmemb:读取文件的块数,读取文件数据总大小为:size * nmemb
stream:已经打开的文件指针
返回值:
成功:实际成功读取到内容的块数,若是此值比nmemb小,但大于0,说明读到文件的结尾。
失败:0
typedef struct _TEACHER{
char name[64];
int age;
}Teacher;
void test(){
//写文件
FILE* fp_write= NULL;
//写方式打开文件
fp_write = fopen("./mydata.txt", "wb");
if (fp_write == NULL){
perror("fopen:");
return;
}
Teacher teachers[4] = {
{ "Obama", 33 },
{ "John", 28 },
{ "Edward", 45},
{ "Smith", 35}
}
for (int i = 0; i < 4; i ++){
fwrite(&teachers[i],sizeof(Teacher),1, fp_write);
}
//关闭文件
fclose(fp_write);

//读文件
FILE* fp_read = NULL;
fp_read = fopen("./mydata.txt", "rb");
if (fp_read == NULL){
perror("fopen:");
return;
}
Teacher temps[4];
fread(&temps, sizeof(Teacher), 4, fp_read);
for (int i = 0; i < 4;i++){
printf("Name:%s Age:%d\n",temps[i].name,temps[i].age);
}
fclose(fp_read);
}

8.2.4.4 格式化读写函数回顾

int fprintf(FILE * stream, const char * format, ...);
功能:根据参数format字符串来转换并格式化数据,而后将结果输出到stream指定的文件中,指定出现字符串结束符 '\0'  为止。
参数:
stream:已经打开的文件
format:字符串格式,用法和printf()同样
返回值:
成功:实际写入文件的字符个数
失败:-1
int fscanf(FILE * stream, const char * format, ...);
功能:从stream指定的文件读取字符串,并根据参数format字符串来转换并格式化数据。
参数:
stream:已经打开的文件
format:字符串格式,用法和scanf()同样
返回值:
成功:实际从文件中读取的字符个数
失败: - 1

注意fscanf遇到空格和换行时结束。 

 

void test(){
//写文件
FILE* fp_write= NULL;
//写方式打开文件
fp_write = fopen("./mydata.txt", "w");
if (fp_write == NULL){
perror("fopen:");
return;
}
fprintf(fp_write,"hello world:%d!",10);
//关闭文件
fclose(fp_write);
//读文件
FILE* fp_read = NULL;
fp_read = fopen("./mydata.txt", "rb");
if (fp_read == NULL){
perror("fopen:");
return;
}
char temps[1024] = { 0 };
while (!feof(fp_read)){
fscanf(fp_read, "%s", temps);
printf("%s", temps);
}
fclose(fp_read);
}

8.2.5.5 随机读写函数回顾

 

int fseek(FILE *stream, long offset, int whence);
功能:移动文件流(文件光标)的读写位置。
参数:
stream:已经打开的文件指针
offset:根据whence来移动的位移数(偏移量),能够是正数,也能够负数,若是正数,则相对于whence往右移动,若是是负数,则相对于whence往左移动。若是向前移动的字节数超过了文件开头则出错返回,若是向后移动的字节数超过了 文件末尾,再次写入时将增大文件尺寸。
whence:其取值以下:
SEEK_SET:从文件开头移动offset个字节
SEEK_CUR:从当前位置移动offset个字节
SEEK_END:从文件末尾移动offset个字节
返回值:
成功:0
失败:-1
long ftell(FILE *stream);
功能:获取文件流(文件光标)的读写位置。
参数:
stream:已经打开的文件指针
返回值:
成功:当前文件流(文件光标)的读写位置
失败:-1
void rewind(FILE *stream);
功能:把文件流(文件光标)的读写位置移动到文件开头。
参数:
stream:已经打开的文件指针
返回值:
无返回值
typedef struct _TEACHER{
char name[64];
int age;
}Teacher;
void test()
//写文件
FILE* fp_write = NULL;
//写方式打开文件
fp_write = fopen("./mydata.txt", "wb");
if (fp_write == NULL){
perror("fopen:");
return;
}
Teacher teachers[4] = {
{ "Obama", 33 },
{ "John", 28 },
{ "Edward", 45 },
{ "Smith", 35 }
};
for (int i = 0; i < 4; i++){
fwrite(&teachers[i], sizeof(Teacher), 1, fp_write);
}
//关闭文件
fclose(fp_write);
//读文件
FILE* fp_read = NULL;
fp_read = fopen("./mydata.txt", "rb");
if (fp_read == NULL){
perror("fopen:");
return;
}
Teacher temp;
//读取第三个数组
fseek(fp_read , sizeof(Teacher) * 2 , SEEK_SET);
fread(&temp, sizeof(Teacher), 1, fp_read);
printf("Name:%s Age:%d\n",temp.name,temp.age);
memset(&temp,0,sizeof(Teacher));
fseek(fp_read, -(int)sizeof(Teacher), SEEK_END);
fread(&temp, sizeof(Teacher), 1, fp_read);
printf("Name:%s Age:%d\n", temp.name, temp.age);
rewind(fp_read);
fread(&temp, sizeof(Teacher), 1, fp_read);
printf("Name:%s Age:%d\n", temp.name, temp.age);
fclose(fp_read);
}

8.4 文件读写案例

8.4.1 读写配置文件

struct info{
char key[64];
char val[128];
};
struct config{
FILE *fp; //保存文件指针
struct info *list; //保存配置信息
int lines; //配置信息条数
};
//加载配置文件
int load_file(char *path, struct config **myconfig){
if (NULL == path){
return -1;
}
//以读写的方式打开文件
FILE *fp = fopen(path, "r+");
if (NULL ==fp){
printf("文件打开失败!\n");
return -2;
}
//配置文件信息分配内存
struct config *conf = (struct config *)malloc(sizeof(struct config));
conf->fp = fp;
conf->list = NULL;
//指针的间接赋值
*myconfig = conf;
return 0;
}
//统计文件行数
int count_file(struct config *config){
if (NULL == config){
return -1;
}
char buf[1024] = { 0 };
int lines = 0;
while (fgets(buf, 1024, config->fp)){
//若是是注释则不统计
if (buf[0] == '#'){ continue; }
lines++;
}
//将文件指针重置到开始位置
fseek(config->fp,0, SEEK_SET);
return lines;
}
//解析配置文件
int parse_file(struct config *config){
if (NULL == config){
return -1;
}
//得到配置文件行数
config->lines = count_file(config);
//给每一行配置信息分配内存
config->list = (struct info *)malloc(sizeof(struct info) * config->lines);
int index = 0;
char buf[1024] = { 0 };
while (fgets(buf, 1024, config->fp)){
//去除每一行最后的\n字符
buf[strlen(buf) - 1] = '\0';
//若是是注释则不显示
if (buf[0] == '#'){
continue;
}
memset(config->list[index].key, 0, 64);
memset(config->list[index].val, 0, 128);
char *delimit = strchr(buf, ':');
strncpy(config->list[index].key, buf, delimit - buf);
strncpy(config->list[index].val, delimit + 1, strlen(delimit + 1));
memset(buf, 0 , 1024);
index++;
}
return 0;
}
const char *get_file(struct config *config, char *key){
if (NULL == config){
return NULL;
}
if (NULL == key){
return NULL;
}
for (int i = 0; i < config->lines;i ++){
if (strcmp(config->list[i].key,key) == 0){
return config->list[i].val;
}
}
return NULL;
}
void destroy_file(struct config *config){
if (NULL == config){
return;
}
//关闭文件指针
fclose(config->fp);
config->fp = NULL;
//释放配置信息
free(config->list);
config->list = NULL;
free(config);
}
void test(){
char *path = "./my.ini";
struct config *conf = NULL;
load_file(path, &conf);
parse_file(conf);
printf("%s\n", get_file(conf, "username"));
printf("%s\n", get_file(conf, "password"));
printf("%s\n", get_file(conf, "server_ip"));
printf("%s\n", get_file(conf, "server_port"));
printf("%s\n", get_file(conf, "aaaa"));
destroy_file(conf);
}

9. 链表

9.1 链表基本概念

9.1.1 什么是链表

  • 链表是一种经常使用的数据结构,它经过指针将一些列数据结点,链接成一个数据链。相对于数组,链表具备更好的动态性(非顺序存储)。

  • 数据域用来存储数据,指针域用于创建与下一个结点的联系。

  • 创建链表时无需预先知道数据总量的,能够随机的分配空间,能够高效的在链表中的任意位置实时插入或删除数据。

  • 链表的开销,主要是访问顺序性和组织链的空间损失。

数组和链表的区别:

数组:一次性分配一块连续的存储区域。

优势:随机访问元素效率高

缺点:1) 须要分配一块连续的存储区域(很大区域,有可能分配失败)

      2) 删除和插入某个元素效率低

链表:无需一次性分配一块连续的存储区域,只需分配n块节点存储区域,经过指针创建关系。

优势:1) 不须要一块连续的存储区域

  2) 删除和插入某个元素效率高

缺点:随机访问元素效率低

9.1.2 有关结构体的自身引用

问题1:请问结构体能够嵌套本类型的结构体变量吗?

问题2:请问结构体能够嵌套本类型的结构体指针变量吗?

typedef struct _STUDENT{
char name[64];
int age;
}Student;
typedef struct _TEACHER{
char name[64];
Student stu; //结构体能够嵌套其余类型的结构体
//Teacher stu;
//struct _TEACHER teacher; //此时Teacher类型的成员尚未肯定,编译器没法分配内存
struct _TEACHER* teacher; //不论什么类型的指针,都只占4个字节,编译器可肯定内存分配
}Teacher;
  • 结构体能够嵌套另一个结构体的任何类型变量;

  • 结构体嵌套本结构体普通变量(不能够)。本结构体的类型大小没法肯定,类型本质:固定大小内存块别名;

  • 结构体嵌套本结构体指针变量(能够), 指针变量的空间能肯定,32位, 4字节, 64位, 8字节;

9.1.3 链表节点

  你们思考一下,咱们说链表是由一系列的节点组成,那么如何表示一个包含了数据域和指针域的节点呢?

  链表的节点类型其实是结构体变量,此结构体包含数据域和指针域:

  • 数据域用来存储数据;

  • 指针域用于创建与下一个结点的联系,当此节点为尾节点时,指针域的值为NULL;

typedef struct Node
{
//数据域
int id;
char name[50];
//指针域
struct Node *next;       
}Node;

9.1.4 链表的分类

  链表分为:静态链表和动态链表

  静态链表和动态链表是线性表链式存储结构的两种不一样的表示方式:

  • 全部结点都是在程序中定义的,不是临时开辟的,也不能用完后释放,这种链表称为“静态链表”。

  • 所谓动态链表,是指在程序执行过程当中从无到有地创建起一个链表,即一个一个地开辟结点和输入各结点数据,并创建起先后相链的关系。

9.1.4.1 静态链表

typedef struct Stu
{
int id;	//数据域
char name[100];
struct Stu *next; //指针域
}Stu;
void test()
{
//初始化三个结构体变量
Stu s1 = { 1, "yuri", NULL };
Stu s2 = { 2, "lily", NULL };
Stu s3 = { 3, "lilei", NULL };
s1.next = &s2; //s1的next指针指向s2
s2.next = &s3;
s3.next = NULL; //尾结点
Stu *p = &s1;
while (p != NULL)
{
printf("id = %d, name = %s\n", p->id, p->name);
//结点日后移动一位
p = p->next; 
}
}

9.1.4.2 动态链表 

typedef struct Stu{
int id;	//数据域
char name[100];
struct Stu *next; //指针域
}Stu;
void test(){
//动态分配3个节点
Stu *s1 = (Stu *)malloc(sizeof(Stu));
s1->id = 1;
strcpy(s1->name, "yuri");
Stu *s2 = (Stu *)malloc(sizeof(Stu));
s2->id = 2;
strcpy(s2->name, "lily");
Stu *s3 = (Stu *)malloc(sizeof(Stu));
s3->id = 3;
strcpy(s3->name, "lilei");
//创建节点的关系
s1->next = s2; //s1的next指针指向s2
s2->next = s3;
s3->next = NULL; //尾结点
//遍历节点
Stu *p = s1;
while (p != NULL)
{
printf("id = %d, name = %s\n", p->id, p->name);
//结点日后移动一位
p = p->next; 
}
//释放节点空间
p = s1;
Stu *tmp = NULL;
while (p != NULL)
{
tmp = p;
p = p->next;
free(tmp);
tmp = NULL;
}
}

9.1.4.3 带头和不带头链表 

  • 带头链表:固定一个节点做为头结点(数据域不保存有效数据),起一个标志位的做用,之后无论链表节点若是改变,此头结点固定不变。

  • 不带头链表:头结点不固定,根据实际须要变换头结点(如在原来头结点前插入新节点,而后,新节点从新做为链表的头结点)。

9.1.4.4 单向链表、双向链表、循环链表

  单向链表:

  双向链表:

  循环链表:

 

9.2 链表基本操做

9.2.1 建立链表

使用结构体定义节点类型:

typedef struct _LINKNODE
{
int id; //数据域
struct _LINKNODE* next; //指针域
}link_node;

  编写函数:link_node* init_linklist() 

  创建带有头结点的单向链表,循环建立结点,结点数据域中的数值从键盘输入,以 -1 做为输入结束标志,链表的头结点地址由函数值返回.

typedef struct _LINKNODE{
int data;
struct _LINKNODE* next;
}link_node;
link_node* init_linklist(){
//建立头结点指针
link_node* head = NULL;
//给头结点分配内存
head = (link_node*)malloc(sizeof(link_node));
if (head == NULL){
return NULL;
}
head->data = -1;
head->next = NULL;
//保存当前节点
link_node* p_current = head;
int data = -1;
//循环向链表中插入节点
while (1){
printf("please input data:\n");
scanf("%d",&data);
//若是输入-1,则退出循环
if (data == -1){
break;
}
//给新节点分配内存
link_node* newnode = (link_node*)malloc(sizeof(link_node));
if (newnode == NULL){
break;
}
//给节点赋值
newnode->data = data;
newnode->next = NULL;
//新节点入链表,也就是将节点插入到最后一个节点的下一个位置
p_current->next = newnode;
//更新辅助指针p_current
p_current = newnode;
}
return head;
}

9.2.2 遍历链表 

  编写函数:void foreach_linklist(link_node* head)

  顺序输出单向链表各项结点数据域中的内容:

//遍历链表
void foreach_linklist(link_node* head){
if (head == NULL){
return;
}
//赋值指针变量
link_node* p_current = head->next;
while (p_current != NULL){
printf("%d ",p_current->data);
p_current = p_current->next;
}
printf("\n");
}

9.2.3 插入节点

  编写函数: void insert_linklist(link_node* head,int val,int data).

  在指定值后面插入数据data,若是值val不存在,则在尾部插入。

//在值val前插入节点
void insert_linklist(link_node* head, int val, int data){
if (head == NULL){
return;
}
//两个辅助指针
link_node* p_prev = head;
link_node* p_current = p_prev->next;
while (p_current != NULL){
if (p_current->data == val){
break;
}
p_prev = p_current;
p_current = p_prev->next;
}
//若是p_current为NULL,说明不存在值为val的节点
if (p_current == NULL){
printf("不存在值为%d的节点!\n",val);
return;
}
//建立新的节点
link_node* newnode = (link_node*)malloc(sizeof(link_node));
newnode->data = data;
newnode->next = NULL;
//新节点入链表
newnode->next = p_current;
p_prev->next = newnode;
}

9.2.4 删除节点

  编写函数: void remove_linklist(link_node* head,int val)

  删除第一个值为val的结点.

//删除值为val的节点
void remove_linklist(link_node* head,int val){
if (head == NULL){
return;
}
//辅助指针
link_node* p_prev = head;
link_node* p_current = p_prev->next;
//查找值为val的节点
while (p_current != NULL){
if (p_current->data == val){
break;
}
p_prev = p_current;
p_current = p_prev->next;
}
//若是p_current为NULL,表示没有找到
if (p_current == NULL){
return;
}
//删除当前节点: 从新创建待删除节点(p_current)的前驱后继节点关系
p_prev->next = p_current->next;
//释放待删除节点的内存
free(p_current);
}

9.2.5 销毁链表 

  编写函数: void destroy_linklist(link_node* head)

  销毁链表,释放全部节点的空间.

//销毁链表
void destroy_linklist(link_node* head){
if (head == NULL){
return;
}
//赋值指针
link_node* p_current = head;
while (p_current != NULL){
//缓存当前节点下一个节点
link_node* p_next = p_current->next;
free(p_current);
p_current = p_next;
}
}

10. 函数指针和递归函数 

10.1 函数指针

10.1.1 函数类型

  经过什么来区分两个不一样的函数?

  一个函数在编译时被分配一个入口地址,这个地址就称为函数的指针,函数名表明函数的入口地址。

  函数三要素: 名称、参数、返回值。C语言中的函数有本身特定的类型。

  c语言中经过typedef为函数类型重命名:

typedef int f(int, int);    // f 为函数类型
typedef void p(int);    // p 为函数类型

  这一点和数组同样,所以咱们能够用一个指针变量来存放这个入口地址,而后经过该指针变量调用函数。 

  注意:经过函数类型定义的变量是不可以直接执行,由于没有函数体。只能经过类型定义一个函数指针指向某一个具体函数,才能调用。

typedef int(p)(int, int);
void my_func(int a,int b){
printf("%d %d\n",a,b);
}
void test(){
p p1;
//p1(10,20); //错误,不能直接调用,只描述了函数类型,可是并无定义函数体,没有函数体没法调用
p* p2 = my_func;
p2(10,20); //正确,指向有函数体的函数入口地址
}

10.1.2 函数指针(指向函数的指针)

  • 函数指针定义方式(先定义函数类型,根据类型定义指针变量);

  • 先定义函数指针类型,根据类型定义指针变量;

  • 直接定义函数指针变量;

int my_func(int a,int b){
printf("ret:%d\n", a + b);
return 0;
}
//1. 先定义函数类型,经过类型定义指针
void test01(){
typedef int(FUNC_TYPE)(int, int);
FUNC_TYPE* f = my_func;
//如何调用?
(*f)(10, 20);
f(10, 20);
}
//2. 定义函数指针类型
void test02(){
typedef int(*FUNC_POINTER)(int, int);
FUNC_POINTER f = my_func;
//如何调用?
(*f)(10, 20);
f(10, 20);
}
//3. 直接定义函数指针变量
void test03(){
int(*f)(int, int) = my_func;
//如何调用?
(*f)(10, 20);
f(10, 20);
}

10.1.3 函数指针数组

  函数指针数组,每一个元素都是函数指针。

void func01(int a){
printf("func01:%d\n",a);
}
void func02(int a){
printf("func02:%d\n", a);
}
void func03(int a){
printf("func03:%d\n", a);
}
void test(){
#if 0
//定义函数指针
void(*func_array[])(int) = { func01, func02, func03 };
#else
void(*func_array[3])(int);
func_array[0] = func01;
func_array[1] = func02;
func_array[2] = func03;
#endif
for (int i = 0; i < 3; i ++){
func_array[i](10 + i);
(*func_array[i])(10 + i);
}
}

10.1.4 函数指针作函数参数(回调函数)

  函数参数除了是普通变量,还能够是函数指针变量。

//形参为普通变量
void fun( int x ){}

//形参为函数指针变量
void fun( int(*p)(int a) ){}
//加法计算器
int plus(int a,int b){
return a + b;
}
//减法计算器
int minus(int a,int b){
return a - b;
}
//计算器
#if 0
int caculator(int a,int b,int(*func)(int,int)){
return func(a, b);
}
#else
typedef int(*FUNC_POINTER)(int, int);
int caculator(int a, int b, FUNC_POINTER func){
return func(a, b);
}
#endif

  注意:函数指针和指针函数的区别:函数指针变量常见的用途之一是把指针做为参数传递到其余函数,指向函数的指针也能够做为参数,以实现函数地址的传递。

  • 函数指针是指向函数的指针;

  • 指针函数是返回类型为指针的函数;


11. 预处理

11.1 预处理的基本概念

  C语言对源程序处理的四个步骤:预处理、编译、汇编、连接。

  预处理是在程序源代码被编译以前,由预处理器(Preprocessor)对程序源代码进行的处理。这个过程并不对程序的源代码语法进行解析,但它会把源代码分割或处理成为特定的符号为下一步的编译作准备工做。

11.2 文件包含指令(#include)

11.2.1 文件包含处理

  “文件包含处理”是指一个源文件能够将另一个文件的所有内容包含进来。C语言提供了#include命令用来实现“文件包含”的操做。

11.2.2 #incude<>和#include""区别

  •  "" 表示系统先file1.c所在的当前目录找file1.h,若是找不到,再按系统指定的目录检索。

  • < > 表示系统直接按系统指定的目录检索。

注意:

  • 1. #include <>经常使用于包含库函数的头文件;

  • 2. #include ""经常使用于包含自定义的头文件;

  • 3. 理论上#include能够包含任意格式的文件(.c .h等) ,但通常用于头文件的包含;

11.3 宏定义

11.3.1 无参数的宏定义(宏常量)

  若是在程序中大量使用到了100这个值,那么为了方便管理,咱们能够将其定义为:

  const int num = 100; 可是若是咱们使用num定义一个数组,在不支持c99标准的编译器上是不支持的,由于num不是一个编译器常量,若是想获得了一个编译器常量,那么能够使用:

  #define num 100

  在编译预处理时,将程序中在该语句之后出现的全部的num都用100代替。这种方法使用户能以一个简单的名字代替一个长的字符串,在预编译时将宏名替换成字符串的过程称为“宏展开”。宏定义,只在宏定义的文件中起做用。

#define PI 3.1415
void test(){
double r = 10.0;
double s = PI * r * r;
printf("s = %lf\n", s);
}

  说明: 

  • 1) 宏名通常用大写,以便于与变量区别;

  • 2) 宏定义能够是常数、表达式等;

  • 3) 宏定义不做语法检查,只有在编译被宏展开后的源程序才会报错;

  • 4) 宏定义不是C语言,不在行末加分号;

  • 5) 宏名有效范围为从定义到本源文件结束;

  • 6) 能够用#undef命令终止宏定义的做用域;

  • 7) 在宏定义中,能够引用已定义的宏名;

11.3.2 带参数的宏定义(宏函数)

  在项目中,常常把一些短小而又频繁使用的函数写成宏函数,这是因为宏函数没有普通函数参数压栈、跳转、返回等的开销,能够调高程序的效率。

  宏经过使用参数,能够建立外形和做用都与函数相似地类函数宏(function-like macro). 宏的参数也用圆括号括起来。

#define SUM(x,y) ((x)+(y))
void test(){
//仅仅只是作文本替换 下例替换为 int ret = ((10)+(20));
//不进行计算
int ret = SUM(10, 20);
printf("ret:%d\n",ret);
}

注意:

  • 1) 宏的名字中不能有空格,可是在替换的字符串中能够有空格。ANSI C容许在参数列表中使用空格;

  • 2) 用括号括住每个参数,并括住宏的总体定义。

  • 3) 用大写字母表示宏的函数名。

  • 4) 若是打算宏代替函数来加快程序运行速度。假如在程序中只使用一次宏对程序的运行时间没有太大提升。

 

11.4 条件编译

11.4.1 基本概念

  通常状况下,源程序中全部的行都参加编译。但有时但愿对部分源程序行只在知足必定条件时才编译,即对这部分源程序行指定编译条件。

11.4.2 条件编译

  n 防止头文件被重复包含引用;

#ifndef _SOMEFILE_H
#define _SOMEFILE_H
//须要声明的变量、函数
//宏定义
//结构体
 
#endif

11.5 一些特殊的预约宏 

  C编译器,提供了几个特殊形式的预约义宏,在实际编程中能够直接使用,很方便。

//    __FILE__    宏所在文件的源文件名
//    __LINE__    宏所在行的行号
//    __DATE__    代码编译的日期
//    __TIME__    代码编译的时间
void test()
{
printf("%s\n", __FILE__);
printf("%d\n", __LINE__);
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);
}

 


 

12. 动态库的封装和使用 

12.1 库的基本概念

  库是已经写好的、成熟的、可复用的代码。每一个程序都须要依赖不少底层库,不可能每一个人的代码从零开始编写代码,所以库的存在具备很是重要的意义。

  在咱们的开发的应用中常常有一些公共代码是须要反复使用的,就把这些代码编译为库文件。

  库能够简单当作一组目标文件的集合,将这些目标文件通过压缩打包以后造成的一个文件。像在Windows这样的平台上,最经常使用的c语言库是由集成按开发环境所附带的运行库,这些库通常由编译厂商提供。

12.2 windows下静态库建立和使用

12.2.1 静态库的建立

 

1. 建立一个新项目,在已安装的模板中选择“常规”,在右边的类型下选择“空项目”,在名称和解决方案名称中输入staticlib。点击肯定。

 

 

2.在解决方案资源管理器的头文件中添加,mylib.h文件,在源文件添加mylib.c文件(即实现文件)。

 

 

3.在mylib.h文件中添加以下代码:

 

#ifndef TEST_H
#define TEST_H
int myadd(int a,int b);
#endif

4.mylib.c文件中添加以下代码:

 

#include"test.h"
int myadd(int a, int b)
{
return a + b;
}

 

5. 配置项目属性。由于这是一个静态连接库,因此应在项目属性的“配置属性”下选择“常规”,在其下的配置类型中选择“静态库(.lib)。

6.编译生成新的解决方案,在Debug文件夹下会获得mylib.lib (对象文件库),将该.lib文件和相应头文件给用户,用户就能够使用该库里的函数了。

12.2.2 静态库的使用

方法一:配置项目属性

A、添加工程的头文件目录:工程---属性---配置属性---c/c++---常规---附加包含目录:加上头文件存放目录。

B、添加文件引用的lib静态库路径:工程---属性---配置属性---连接器---常规---附加库目录:加上lib文件存放目录。

C  而后添加工程引用的lib文件名:工程---属性---配置属性---连接器---输入---附加依赖项:加上lib文件名。

方法二:使用编译语句

#pragma comment(lib,"./mylib.lib")

方法三:添加工程中

就像你添加.h和.c文件同样,把lib文件添加到工程文件列表中去.

切换到"解决方案视图",--->选中要添加lib的工程-->点击右键-->"添加"-->"现有项"-->选择lib文件-->肯定.

12.2.3 静态库优缺点

静态库对函数库的连接是放在编译时期完成的,静态库在程序的连接阶段被复制到了程序中,和程序运行的时候没有关系;

程序在运行时与函数库再无瓜葛,移植方便。

浪费空间和资源,全部相关的目标文件与牵涉到的函数库被连接合成一个可执行文件。

内存和磁盘空间

  静态连接这种方法很简单,原理上也很容易理解,在操做系统和硬件不发达的早期,绝大部门系统采用这种方案。随着计算机软件的发展,这种方法的缺点很快暴露出来,那就是静态连接的方式对于计算机内存和磁盘空间浪费很是严重。特别是多进程操做系统下,静态连接极大的浪费了内存空间。在如今的linux系统中,一个普通程序会用到c语言静态库至少在1MB以上,那么若是磁盘中有2000个这样的程序,就要浪费将近2GB的磁盘空间。

程序开发和发布

  空间浪费是静态连接的一个问题,另外一个问题是静态连接对程序的更新、部署和发布也会带来不少麻烦。好比程序中所使用的mylib.lib是由一个第三方厂商提供的,当该厂商更新容量mylib.lib的时候,那么咱们的程序就要拿到最新版的mylib.lib,而后将其从新编译连接后,将新的程序整个发布给用户。这样的作缺点很明显,即一旦程序中有任何模块更新,整个程序就要从新编译连接、发布给用户,用户要从新安装整个程序。

12.3 windows下动态库建立和使用

  要解决空间浪费和更新困难这两个问题,最简单的办法就是把程序的模块相互分割开来,造成独立的文件,而不是将他们静态的连接在一块儿。简单地讲,就是不对哪些组成程序的目标程序进行连接,等程序运行的时候才进行连接。也就是说,把整个连接过程推迟到了运行时再进行,这就是动态连接的基本思想。

12.3.1 动态库的建立

1. 建立一个新项目,在已安装的模板中选择“常规”,在右边的类型下选择“空项目”,在名称和解决方案名称中输入mydll。点击肯定。

2.在解决方案资源管理器的头文件中添加,mydll.h文件,在源文件添加mydll.c文件(即实现文件)。

3.在test.h文件中添加以下代码:

#ifndef TEST_H
#define TEST_H
__declspec(dllexport) int myminus(int a, int b);
#endif

5.test.c文件中添加以下代码:

 

#include"test.h"
__declspec(dllexport) int myminus(int a, int b){
return a - b;
}

 

5. 配置项目属性。由于这是一个态连接库,因此应在项目属性的“配置属性”下选择“常规”,在其下的配置类型中选择“态库(.dll)。

6.编译生成新的解决方案,在Debug文件夹下会获得mydll.dll (对象文件库),将该.dll文件.lib文件和相应头文件给用户,用户就能够使用该库里的函数了。

疑问一:__declspec(dllexport)是什么意思?

动态连接库中定义有两种函数:导出函数(export  function)和内部函数(internal  function)。 导出函数能够被其它模块调用,内部函数在定义它们的DLL程序内部使用。

疑问二:动态库的lib文件和静态库的lib文件的区别?

在使用动态库的时候,每每提供两个文件:一个引入库(.lib)文件(也称“导入库文件”)和一个DLL(.dll)文件。虽然引入库的后缀名也是“lib”,可是,动态库的引入库文件和静态库文件有着本质的区别,对一个DLL文件来讲,其引入库文件(.lib)包含该DLL导出的函数和变量的符号名,而.dll文件包含该DLL实际的函数和数据。在使用动态库的状况下,在编译连接可执行文件时,只须要连接该DLL的引入库文件,该DLL中的函数代码和数据并不复制到可执行文件,直到可执行程序运行时,才去加载所需的DLL,将该DLL映射到进程的地址空间中,而后访问DLL中导出的函数。

12.3.2 动态库的使用

方法一:隐式调用

建立主程序TestDll,将mydll.h、mydll.dll和mydll.lib复制到源代码目录下。
(P.S:头文件Func.h并非必需的,只是C++中使用外部函数时,须要先进行声明)
在程序中指定连接引用连接库 : #pragma comment(lib,"./mydll.lib") 

方法二:显式调用

HANDLE hDll; //声明一个dll实例文件句柄
hDll = LoadLibrary("mydll.dll"); //导入动态连接库
MYFUNC minus_test; //建立函数指针
//获取导入函数的函数指针
minus_test = (MYFUNC)GetProcAddress(hDll, "myminus");

 

12. 递归函数

12.1 递归函数基本概念

  C经过运行时堆栈来支持递归函数的实现。递归函数就是直接或间接调用自身的函数。

12.2 普通函数调用

void funB(int b){
printf("b = %d\n", b);
}
void funA(int a){
funB(a - 1);
printf("a = %d\n", a);
}
int main(void){
funA(2);
    printf("main\n");
return 0;
}

函数的调用流程以下:

12.3 递归函数调用

 

void fun(int a){
if (a == 1){
printf("a = %d\n", a);
return; //中断函数很重要
}
fun(a - 1);
printf("a = %d\n", a);
}
int main(void){
fun(2);
printf("main\n");
return 0;
}

函数的调用流程以下:

 

 做业:

递归实现给出一个数8793,依次打印千位数字八、百位数字七、十位数字九、个位数字3。
void recursion(int val){
if (val == 0){
return;
}
int ret = val / 10;
recursion(ret);
printf("%d ",val % 10);
}

12.4 递归实现字符串反转

 

int reverse1(char *str){
if (str == NULL)
{
return -1;
}
if (*str == '\0') // 函数递归调用结束条件
{
return 0;
}
reverse1(str + 1);
printf("%c", *str);
return 0;
}
char buf[1024] = { 0 };  //全局变量
int reverse2(char *str){
if (str == NULL) 
{
return -1;
}
if ( *str == '\0' ) // 函数递归调用结束条
{
return 0;
}
reverse2(str + 1);
strncat(buf, str, 1);
return 0;
}
int reverse3(char *str, char *dst){
if (str == NULL || dst == NULL) 
{
return -1;
}
if (*str == '\0') // 函数递归调用结束条件
{
return 0;
}
reverse3(str + 1);
strncat(dst, str, 1);
return 0;
}

13. 面向接口编程

13.1案例背景

  通常的企业信息系统都有成熟的框架。软件框架通常不发生变化,能自由的集成第三方厂商的产品。

13.2 案例需求

  要求在企业信息系统框架中集成第三方厂商的socket通讯产品和第三方厂商加密产品。软件设计要求:模块要求松、接口要求紧。

13.3 案例要求

  • 1)能支持多个厂商的socket通讯产品入围

  • 2)能支持多个第三方厂商加密产品的入围

  • 3)企业信息系统框架不轻易发生框架

13.4 编程提示

  • 1)抽象通讯接口结构体设计(CSocketProtocol

  • 2)框架接口设计(framework)

  • 3)

    • a) 通讯厂商1入围(CSckImp1
    • b) 通讯厂商2入围(CSckImp2)
  • 4)

    • a) 抽象加密接口结构体设计(CEncDesProtocol

    •  b) 升级框架函数(增长加解密功能)

    • c) 加密厂商1入围(CHwImp)、加密厂商2入围(CCiscoImp)

  • 5)框架接口分文件
相关文章
相关标签/搜索