学习C语言

一.PE格式

1.PE文件

在win下:.exe .dll .sys

在linux下: elf

此类文件都属于PE结构的文件类型。

2.PE文件的构成

由DOS部首,PE文件头,块表(section table),块(section)以及调试信息组成

DOS部首

最开始的部分是DOS部首,由DOS的MZ文件标志和DOS stub组成

(MZ)开头: e_magic ,为固定的值 “0x5a4d”

DOS Stub即为一句话”This program Cannot be run in DOS mode”

PE文件头

最开始的部分是以’PE\0\0’:”0x50 0x45 00 00”开始的,由DOS部首中,结构成员e_lfanew(0x3c)指向

在PE文件头中,IMAGE_OPTIONAL_HEADER32和IMAGE_FILE_HEADER都定义了很多PE的数据

特别在IMAGE_OPTIONAL_HEADER32中,包含了数据结构表

数据结构表有导出表和导入表

导入表为引用的函数,导出表则为程序本身定义的函数

块表

块表指向块

RVA(相对虚拟地址)= VA(虚拟地址) - ImageBase(起始地址)

VOFFSET = 每个节的虚拟地址 - 起始地址

ROFFSET = 每个节的虚拟地址 - 每个文件的首地址

二.进制转换

1.表现形式

在C语言中:16进制为0x开头,如(0x71ef),x可以为大小写

在汇编语言中:

16进制为H结尾,如:(1aH)

八进制为O结尾,如:(17O)

十进制为D结尾,如:(12D)

二进制为B结尾,如:(110B)

2.转换

计算方法。。。。。。

3.整数编码

表格法:1024 512 256 128 64 32 16 8 4 2 1 可以计算整数的原码

计算机使用补码

补码中负数等于绝对值的原码取反加一

4.整数的存储方式

但是,x86(32位)系统则为低位优先,按图,则是78 56 34 12

5.浮点数的定义和储存

float x = 1.732f;
double y = 3.1415926;

浮点数需要在常数后面加f声明为浮点数,双精度则不需要

单精度在二进制存储当中阶码部分占用8位,小数部分中占用23位

双精度在二进制存储当中阶码部分占用11位,小数部分中占用52位,两种精度都各保留一位符号位

0为正数,1为负数

三.C语言中数据的类型

1.数据类型

字符类型: char (ASCLL字符) / wchar_t (UNICODE字符) UNICODE字符需要在值前加L来声明

整型: short/int/long/long long/_int64

实数:单精度float ,双精度double

有符号和无符号:signed/unsigned, (signed)int/(unsigned) int

布尔类型:bool , 0/1(true/false),C99<stdbool.h>

2.Ascll编码

其中英文大写和小写之间的差值为32,可以用代码表示为

char c = 'M';
c + 'a'-'A';
//大写转小写
char d = 'm';
d + 'A'-'a'
//小写转大写

3.整型

int 和long int(long)占4个字节,long在linux里占8个字节

longlong(gc) 和 _int64(vc)占8个字节

bool的长度为1个字节,BOOL为4个字节

long a = 100L;
unsigned long b = 100UL;
long long c = 100ll;        //linux
_int64 d = 100i64;          //windows

4.自定义新的类型

typedef int INT
typedef unsigned int UINT             //重新定义类型,好处简洁,同一编码风格,容易在其他平台改动

5.范围

char [0,255],8位, short [0,65535] 16位

int [0,4294967295] 32位

以上均为无符号,有符号范围则是 [ -2 ^(n-1),2^(n-1)-1 ]

*在linux中,超级用户的GID为0(数据溢出,即如果超过可表示的范围则从负数开始计算)

6.类型转换

强制转换,人为使变量的类型发生转变,用例:

char c = 'a';					//原类型
short i = (short)c;             //转换

浮点数到整数,如:

int i = (int)3.14;
int b = (int)3.74;                       //两个结果均为3,只取整,并不会四舍五入

如果从大到小转换

int x = 0x12345678;
short y = (short)x;

数据会从高位开始截断,如上,int为4个字节的数据,而short为两个字节,所以,y的值则会是0x5678

自动转换,遵循一个规则,所有运算中,必须全部转换成同一种类型的数据再进行运算

且自动转换是以运算中最长的一种类型统一转换

单精度转双精度则是通过截尾来实现,会丢失一定量的数据(要进行四舍五入操作)

若signed整型赋给长度相同的unsigned型变量,内部存储形式不变,但外部表示时总是无符号的

short s = -1;
unsigned short us =(unsigned short)s;
us = 65535

四.变量

1.变量命名和定义

命名原则:只能由字母,下划线和数字组成,但第一个字符必须为字母,下划线也被看做为字母,且不能用关键字来当做变量,大写字符一般用于定义常量

命名规则(例:匈牙利命名)

一般有4种命名方法:匈牙利,下划线(linux),驼峰,帕斯卡

int iMyData;     //匈牙利
int my_data;     //下划线
int myData;      //驼峰
int MyData;      //帕斯卡

2.变量类型

共有:全局变量,全局静态变量,局部变量,局部静态变量,寄存器变量5种

五.开始使用

1.输入与输出

使用scanf函数接收输入

用法(例)

int C;
scanf("%c",&c);              //warning:4996,可能会出现安全问题,可使用scanf_s()
printf("%c",c);

在连续两个接收数据时,因为用户按下了回车,而回车也会当做一个字符,所以会导致后面的接收被顶掉,所以应该在每次scanf后输入:

fflush(stdin);                  //清空数据缓存区域
//或者
scanf(" %c",%c);                //在%c前加一个空格

还有其他接收字符的函数,例:

char a = getch();              //检测输入后自动下一步
char a1 = getchar();           //等待回车

也可以用_getch()代替

%号后面的值:(tips: hd =短整型,ld = 长整型,I64d = _int64的整数)

浮点数tip: %.[数字]可以控制浮点数后面有几位小数

字符串:

gets();                          //可以用gets_s()代替
puts();                          //可以避免scanf以空格结束的问题

文件流: stdin(输入),stdout(输出),stderr(错误)

2.运算符

()优先级最高,优先算括号内数

逻辑非! : !0=1,!非零 = 0

自增,自减: i++,++i,i–,–i

i++和++i的区别

int i = 0;
int a = i++;                  //先i=a,在i+1
int b = ++i;                  //先i+1,再i=b

&&(逻辑与,且),||(逻辑或),&&的优先级大于||

?:取两个数的最大值

a<b?a:b                   //如果前式为真,则取a,为否,则取b

,运算符是优先级最低的

贪心算法:

编译器从左到右开始对运算符号依次读取,直到无法再组成一个新的运算式,例

a+++++b;                  //解析结果为((a++)++)+b

3.随机数

rand函数,但是需要随机生成一个种子,用例(一百以内的随机数):

srand((unsigned int)time(0));               //取当前时间为随机数的种子
for(int i=0;i<10;i++);{
    printf("%d",rand()%100);                 //除以100的余数,即余数小于100,得到100以内的随机数
}

4.switch语句

与if语句使用方法类似,下面为例:

switch()
{
case 'A';
    do something;
	break;                      //如果没有加break的话,语句会继续往下执行而不会跳出switch函数
case 'B';
    do something;
    break;
...
}

5.循环

for()语句由三个部分组成( 初始化变量 ; 判断条件; 更新循环变量表达式 )

do while 循环

do                //int i = 0 
{
    printf("balabala");
    i++;
}while (i<10);                             //与while循环不同的是,是先执行,再进行判断

6.转向语句

1.goto语句(常用于出错处理,跳出多重循环,慎用)

goto L1;
...
L1;
语句
语句

tip: 如果申请内存使用后不再需要,要记得free()释放内存,防止内存泄漏

2.break和continue

break函数用于退出循环,return用于退出整个函数,并提供返回值

continue用于进入下一次循环,当执行到continue语句时,当前循环语句将不会继续进行

六.数组

1.一维数组

1.随机访问,数组中每个数代表4个字节,那么就可以做到:

int a[10];               //假如要访问数组中第6个值
a[0]+20;                 //加20得到的即为第六个值的地址

2.初始化

int a[10] = {1,2,3,4,5,6,7,8,9}
int a[10] = {0,1,2,3,4}                         //没有初始化的值均为0
int a[10] = {(0,1),(2,3),4}                     //逗号表达式,结果等价于{1,3,4}
int a[10] = {0}                                 //全部初始化为0
int a[]={1,2,3,4,5}

3.字符数组

char str1[] = {'h','e','l','l','0'};
char str2[] = "hello"//看起来相同,但是str2比str1多1个\0作为结束符

tip:数组名是常量指针,一旦定义,就不能修改,如:

char a[100];
a = "hello world"                          //error

2.二维数组

定义方式:(因为量大,不好直接写,直接放图)

初始化:

int a[5][3] = {{23,34,21},{72,2,31},{123,23,5},...};

3.注意事项

在数组中,如果有整数型数组a1[5]={0}; a2[3][4]={0};假设a1的起始地址为:10000000,a2的起始地址为200000000

对数组进行+1操作可以得到:

a1 + 1 = 100000004;           &a1 + 1 = 200000020;
a2 + 1 = 100000016;           &a2 + 1 = 200000048;

在数组中,一行算一个元素,一个元素4个字节,a1+1的值则加一个元素,而&a1为数组的地址,则加一整个地址,有5个元素,则加4*5=20

a2有4行,算4个元素,则a2+1的地址加16,&a2则有总共12个元素,则地址加3*4*4=48

//P26 30:30

数组的溢出

C编译器对数组溢出不做检测
程序在运行时候,数组溢出导致程序行为未定义

4.数组的应用

1.计算斐波那契数组

2.字符串大小写转换

3.计算平均值

定义一个数组,通过for循环遍历整个数组,令数组不断自增,最后再除以数组中数据的数量

4.取最大值

七.字符串

C语言中字符串可以定义为:”c1,c2.c3…..cn \0”其中\0是结束符,算作一个字符

1.转义字符

字符串(“a”)和字符常量(‘a’)的区别

字符串是存储在静态区的,”a”对应的是字符串的首地址,所以可以赋值给字符指针

而字符常量’a’只是赋值给变量,不存储在静态区域,没有内存,不可赋值给字符指针

2.宽字符串

wchat_t类型

wchar_t a = L"Hello,世界"

宽字符串相比普通字符串,其中所有的字符都占两个字节,但是响应速度更快

3.使用malloc函数动态分配内存调用字符串

用例:

把字符串存放在堆上来调用字符串

这样,可以通过指针移动来遍历整个字符串,知道遇到字符’\0’

4.字符串做函数参数

void print_str(char *str){
    while(*str != '\0'){
        printf("%c",*str);        //定义一个函数,不断遍历指针*str的字符,直到遇到'\0'
        str++;
    }
}
int main(void){
    char *str = "hello world";       //向指针中传递参数,用定义的函数print_str输出
    print_str(str);
    return 0;
}

5.字符串api

有3套库函数,strxxx/wcsxxx/_tcsxxx,建议使用tchar的写法(兼容多字节字符集工程和UNICODE字符集工程):

#include <tchar.h>
TCHAR *tstr = _T("Hello world");
  • strcpy 将字符串拷贝到另一个变量或数组
strcpy(要拷贝到的变量或数组,原位置);
strcpy(buf1,s1);
_tcscpy(buf2,s2);
  • strcmp比较两个字符串
strcmp(s1,s2);
strncmp(s1,s2,5);        //用于比较字符串中前几个字符是否相等
stricmp(s1,s2);          //忽略大小写
strnicmp(s1,s2,3);       //与strncmp相同,忽略大小写
  • strcat拼接两个字符串
char path[260] = "c:\\doc\\test\\";
char *filename = "meow.txt";
strcat(path,filename);
printf("path:%s\n",path);
  • strchr(strrchr)查找
char *p = strchr(path,'x');

用于从左到右查找字符串中的一个字符是否存在,若存在,则输出以后的所有内容(如果是strrchr则是从右往左)

同样的strstr可以查找字符串,用法类似

  • strtok分割(两个参数,strtok(要分割的字符串,分割符))

  • atoi(字符串转整型)/atof(字符串转浮点)/atol(字符串转长整型)/atoll(字符串转long long)/_ttol(针对TCHAR)

八.函数

在C中,把为了实现某一特定的功能的所有语句归纳在一起,就形成了一个函数。一般来说,函数只实现单一功能

引用自己的函数:

1.创建一个.cpp源文件,将函数写进.cpp文件中,函数在写的时候需要一个返回值,ruturn函数计算的结果

2.创建一个.h头文件,在头文件中写入引用的函数(#pragma once是用于同一个头文件被包含多次)

同时,ifndef也可以保证头文件只被一次包含

#ifndef _Exemple_
#define _Exemple_
....(包含的函数)
#endif

3.使用(#include “要包含的自己创建的头文件”)

使用

1.命令行参数

int _tmain(int argc,_TCHAR* argv[]){
    ...
}

其中,argc为命令行参数的个数,argv[]存放命令行参数

2.函数的注意事项(模块化,方便调试,维护)

  • 变量初始化,在函数局部变量要保证初始化
  • 严进宽出: 在一开始排除非法数据,后面轻松
  • assert:对参数进行断言
  • 时间与空间复杂度: 内存少,使用快,尽量优化,不要分配内存(malloc)
  • 边界考虑: 对条件充分考虑,避免特殊情况发生
  • 功能测试: 在不同用例中测试函数的功能是否运行正常

3.库函数

库函数都有官方的说明文档,可以通过官方文档来进行传参,调试

4.errno_t函数

errno_t函数可以用来查看错误码(存放在头文件Windows.h中),用例:

#include <Windows.h>
#include <stdio.h>
...
errno_t err = GetLastError();
printf("err: %d",err);

例:传回来的错误码可以在工具Error Lookup(错误查找)中查看(visual stdudio):

5.面向过程和面向对象

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

两者的区别是:

面向对象,所有的动作都对应一个对象

面向过程,每个动作都是从一个动词开始的,每个动作对应一个函数

6.函数的传参

3种方式

1.传值: 形参是对实参值的一个拷贝,形参和实参是不相关。无法通过改变形参来改变实参

2.传指针: 形参是对实参地址的一个拷贝,通过地址可以实现对实参的修改

3.传引用: 形参是对实参的一个引用(别名),形参就是实参本身,改变形参就是改变实参本身

函数用参数作为返回值

  • 变量既是输入,也是输出参数
void add(int *x,int y);          //*x = *x + y;

数组做函数参数,防溢出:

7.*函数的调用约定

默认调用约定:

int func (int x, int y);

cdecl调用约定:(参数入栈顺序:从右到左。调用者修改栈,所以可以支持变参函数,因为能恢复栈平衡)

int__cdecl func(int x, int y);               //先是y入栈,再是x

stdcall调用约定:(从右往左压入栈。被调用函数自身修改栈)

int__stdcall func(int x,int y);

fastcall调用约定:(函数的第一个和第二个通过ecx和edx传递,剩余参数从右到左入栈。被调用者修改栈)

int__fastcall func(int x,int y,int z);

的增长方向和内存增长方向相反

先是参数入栈,然后是返回地址入栈(eip)(调用完当前函数后下一条要执行的指令),接着是ebp寄存器入栈(debug版本),最后是esp寄存器(占领寄存器),然后得到局部变量区的空间。
函数调用完之后,开始出栈,esp到达参数区域时,要使内存完全释放,需要+(对应参数的值)*4+4

每个参数在入栈时都会被提升至4个字节

x64平台统一使用fastcall约定

区别:

8.inline(内联函数)

优点:没有栈操作,运行效率高
缺点:代码会变大

9.static关键字

static(静态),限制函数只在当前文件下使用,防止命名冲突

10.函数设计的常见问题

1.printf打印结果代替返回值

输出结果返回给调用者,printf没有任何意义,调用者是看不到的

2.逻辑全部或者部分放在了main函数

算法必须单独写成普通函数,然后在main函数里测试。main里不能有算法逻辑或者功能部分,main只负责数据测试

3.调用了库函数

4.代码缺少封装

5.函数内部内存分配

6.硬编码

7.指针移动

一块N个字节的内存,它的首地址(头指针)为pstart,那么末地址为:

pStart+N-1;

九.指针

指针就是一个变量(x86 占4个字节,x64占8个自己字节),它与其他变量的不同就在于它的值是一个内存地址,指向内存的某一个地方,明确了该内存的宽度(通过指针类型确定)。指针含义分为3个方面:(变量&&地址&&内存宽度)

*解引用(dereference)运算符

通过指针(存放的内存地址),找到对应的内存和里面存放的数据类似于邮递员根据信封地址,找到地点

&和*互为逆运算

*&与&*(&:取变量的地址,*:取地址对应的内存),如果为void类型,那么长度不确定,GCC中默认为1字节

int a = 10;

*&a == a;
&*a;   //error

int *p = &a;
*p == a;
*&p == p;
&*p == p;

二级指针

一级指针中存放的是普通变量的内存地址,二级指针中存放的是一级指针的地址

作用:传参是改变一级指针的值

传参:

int func1(int x);          //传实参值,不能改变实参
int func2(int *x);         //穿实参指针,修改实参
int func3(int &x);         //传实参引用,修改实参
int func4(int **x);        //实参是指针,传指针的指针,修改指针
int func5(int *&x);        //实参是指针,传指针的引用,修改指针

十.内存

系统虚拟内存空间布局:

内存分类:

  • 堆heap
  • 栈stack
  • 静态区
  • 代码区

图为内存由上往下增长

1.堆和栈的区别

内存分配

  • 栈:由系统自动分配与回收,int b = 0;增长由高到低
  • 堆: malloc/free,地址由低到高

大小限制

  • 栈:应用层1M到10M,内核层: 12k到24k不等
  • 堆:受限于计算机系统中有效的虚拟内存

效率比较

  • 栈:由系统自动分配,速度较快。但是程序员无法控制
  • 堆:速度比较慢,而且容易产生内存碎片

存放内容:

  • 栈:栈是用来记录程序执行时函数调用过程中的活动记录(栈帧),参数,返回地址,ebp,局部变量等
  • 堆:一般是在堆的头部用一个自己存放堆的大小,剩余部分存储的内容由程序员根据程序计算的需要决定

2.内存地址分类和寻址模式

1.逻辑地址,线性地址,物理地址

  • 逻辑地址是编译器生成的,使用C语言指针时,指针的值就是逻辑地址。逻辑地址由段地址+段内偏移组成

  • 线性地址是有分段机制将逻辑地址转化而来的。

  • 物理地址是CPU在存取数据时最终在地址总线上发出的电平信号,靠改地址来访问对应数据

2.内存的寻址模式

  • 扁平模型
  • 分段模型
  • 实模式
  • 保护模式

3.内存分配

malloc/calloc/relloc

void *malloc(unsigned int num_bytes)
  • num_bytes:分配的内存字节数
  • 失败返回NULL,成功返回内存地址,内存中为垃圾值,需要清零,用free释放
void *calloc(size_t nelem,size_t elsize);
  • nelem:元素个数
  • elsize:元素长度
  • 分配的内存会被初始化为0,free释放
void *realloc(void *mem_address,unsigned int newsize);

先判断当前指针是否有足够的连续空间,如果有,扩大mem_address指向的地址,并且将mem_address返回,如果空间不够。先按照newsize指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来mem——address所致内存区域,同时返回新分配的内存区域的首地址,记得free释放

4.内存泄漏

动态分配的内存在程序结束后而已值未释放,就出现了内存蟹柳。一般场合说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的,使用完必须释放的内存。应用程序一般使用malloc,new等函数从堆中分配到一块内存,使用完后,程序必须负责响应的调用free或菏泽delete释放该内存块,否则,这块内存就不能被再次使用,就说这块内存泄漏了(频繁的内存泄漏,将最终耗尽珍格格内存资源,让系统性能大幅下降)

几种内存泄漏的情况:

1.分配了内存没有释放

2.调用了不正确的系统api

3.打开了句柄但是未关闭

预防内存泄漏

1.malloc/free 和 new/delete要配对出现

2.分支退出别忘记释放已分配的内存,(goto),例:

char *p1=(char *)malloc(64);
if(p1 == NULL){
    goto err;
}
char *p2 = (char *)malloc(128);
if(p2==NULL){
    goto err;
}
err:
	if(p1){
        free(p1);
    }							//用goto函数集中释放内存
	if(p2){
        free(p2);
    }

3.一般函数内部如果一定要分配内存,那么最好是在函数内释放内存,不要返回堆上的内存

4.复杂引用使用引用计数

5.C++中使用智能指针

十一.结构体

定义:struct(结构体),是由若干个“成员”组成的(每个成员可能是不同类型的数据),每一个成员可以是一个基本数据类型或者又是一个构造类型,结构体在底层编程中大量存在

基本的定义方法(例):

访问结构体中的内容(使用指针需要使用箭头来访问结构体中的内容):

*与->和.运算符

因为->与.运算符比*的优先级高,所以如果没有括号,先算->和.

//ps2是一个指针
printf("id:%s",*ps2.id);     //报错,认为id前面没有类型
printf("id:%s",(*ps2).id);   //成功,因为*ps2=s2,等价于s2.id
printf("id:%s",ps2->id);     //一般用法

tip:如果结构体内有指针,需要让指针指向一个有效的内存

2.浅拷贝和深拷贝

浅拷贝只拷贝结构体的字段值,包括指针类型成员的地址。也就是说,浅拷贝只复制指针的值(即它所指向的内存地址),但不复制指针所指向的实际数据。

深拷贝不仅拷贝结构体的字段值,还拷贝指针所指向的数据。也就是说,深拷贝为指针指向的实际数据分配新的内存,并将数据拷贝到新内存中。

如果分配了内存,浅拷贝会导致被拷贝的变量释放内存后,接收拷贝的变量指向的值无效

浅拷贝:

Person p2 = p1;  // 浅拷贝,将person中p1的值复制给p2

深拷贝:

s1.a = 10;                            //tip: s1和s2都为一个同一个结构体的变量
s1.p = 10;
s1.p = (cahr*)malloc(100);
s2.a = s1.a;
s2.p = (char*)malloc(100);            //分配内存
memcpy(s2.p, s1.p, 100);              //复制s1.p的内存到s2.p的内存中  
free(s1.p)                            //内存释放后s2.p的值不会受到影响

3.结构体的应用

结构体的遍历

输出结果: