C语言重点复习大纲

数据存储(3星)

判断大小端

在这里插入图片描述

大端

将数据的高位存储在内存的低地址;

小端(常用)

数据的低位存储在内存的低地址

写一个函数判断大小端

强转即可;发生了截断;

bool judge(int i = 1){
   //判断是不是小端
    return char(i);
}

截断与整形提升

截断

    int a = 0x0fffffff;
    char c = a;
    int b = c;

    printf("%dn", b);// char 1个字节 发生截断,截取低位8个字节(11111111),(注意大小端),输出-1

整形提升

char a = 0xff;//8个1
printf("%dn",a); //输出-1;a二进制 11111111 ,符号位为1,提升int4字节的时,前面补1!

a= 0x7f;
printf("%dn",a); //输出127;a二进制 01111111 ,符号位为0,提升int4字节的时,前面补0

数组和指针(5星)

数组

一段连续的内存空间,用于存储若干个指定相同类型的值;

只有在&arr 或者 sizeof(arr)的时候,数组名arr才被看作整个数组,其余情况看做首元素的地址!

指针

指向内存中的一个地址,该地址对应的内存空间下可能存放有特定数据;

几个特殊的指针

注意符号的优先级进行区分!

指针数组

int *arr[NUM]; //[]优先,所以他是一个数组,存储的类型为int*;

数组指针

int (*arr)[NUM]; //()让arr是一个指针,指向的类型为int [NUM]数组 

函数指针

int (*ptr)(int int) //ptr是一个指针,指向int (int int),参数为两个int,返回值为int的函数;
    
//这样比较复杂,一般typedef定义一个新的类型名;
typedef int (*Ptr)(int int) Ptr
Ptr p = Add;//创建实例;
(*p)(1,1);//调用函数Add 返回2

数组传参

一维数组传参相当于退化为指针;

void f1(int arr[])//int *arr
{
	
    cout<<sizeof(arr)<<endl; //输出4
}
int main()
{
    int arr[10];
    f1(arr);
}

二维数组传参必须指定列数(才能确定+,-这些跳过二维中若干个一维的地址偏移操作),相当于数组指针;

void f2(int arr[][4])//int *arr
{
	
    cout<<sizeof(arr)<<endl; //输出4
}
int main()
{
    int arr[4][4];
    f2(arr);
}

字符串数组

假设字符串数组为carr,这里要注意strlen(carr)和sizeof(carr)的区别;

  • 因为strlen计算字符串长度,他会找‘’然后停下,所以不会计算’’;
  • sizeof则会把’’这个特殊字符也算上,所以对于一般的字符串,sizeof(carr) = strlen(carr)+1;

库函数的实现(4星)

atoi与itoa

atoi

int atoi (const char * str);
//将一个字符串转换成int返回;



int My_Atoi(const char* str){
    
    
    int len = strlen(str);
    char tmp[100];
    int index = 0;
    for(int i = len-1;i>=0;i--){
        if(str[i]=='-') continue;//跳过负数,最后再判断;
        tmp[index++] = str[i];
    }
    int ret = 0;
    while(len)
    {
        int c = tmp[len-1] - '0';
        ret*=10;
        ret+=c;
        len--;
    }
    
   //判断负数情况
    if(str[0] == '-') ret*=-1;
    return ret;
}

itoa

char *  itoa ( int value, char * str, int base );
//将一个整形转换成对应进制的字符串; 进制转换<-->辗转相除;





void Swap(char* a, char* b)
{
    char tmp = *a;
    *a = *b;
    *b = tmp;
}
char* My_Itoa(int value, char* str, int base)
{
    char ret[1024];
    int index = 0;
    int flag = 0;
    char carr[] = { '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F' };
    //处理负数用'-'加原码表示,暂时不考虑补码这些;
    if (value < 0) {
        flag = 1;
        value *= -1;
    }

    while (value) 
    {
        ret[index++] = carr[value % base];
        value /= base;
    }
    if (flag) {
        ret[index++] = '-';
    }
     
    //翻转
    int l = 0;
    int r = index - 1;
    while (l < r)
    {
        Swap(&ret[l],&ret[r]);
        l++;
        r--;
    }
   
    ret[index++] = '';


    return ret;
}


memcpy与memmove

strcpy用于string的拷贝,遇到’’停止,使用仅限于字符串;

二momcpy是void*类型的内存拷贝,适用更多场景;

在C/C++一些笔试中较常见要求不用标准库函数,实现mommove函数的功能,这里进行一下自我总结:

void * memcpy ( void * dest, const void * src, size_t num );
void * memmove ( void * dest, const void * src, size_t num );
  • dest 目的内存首地址;
  • src (资)源内存首地址;
  • num 拷贝字节数;
  • 返回值:最开始的dest的首地址

可以看到memcpy与memmove的返回值或者参数都是一样的,其实在一些编译器中,memcpy已经被优化为了memmove;

两者区别:

memmove是momcpy的升级版,memcpy不处理内存重叠时可能引发的问题,出现重叠情况可能会拷贝紊乱出错(src<dest);而memmove处理了内存重叠时可能引发的问题!;

C还保留memcpy的原因是,让之前用过memcpy的代码能正常运行;

memcpye

void * memcpy ( void * dest, const void * src, size_t num );

//

//不处理内存重叠中的特殊情况;
void* my_memcpy( void * dest, const void * src, size_t num )
{
    void* ret = dest;
    while(num--){
        *(char*)dest = *(char*)src;
        dest = (char*)dest + 1;
		src = (char*)src + 1;;
    }
    return ret;
}

int main()
{
	int arr[] = {1,2,3,4,5,6,7,8,9,10};
	
    my_memcpy(arr,arr+5,5*4);//6,7,8,9,10,6,7,8,9,10 //内存重叠,但是src<dest,没问题
    my_memcpy(arr+1,arr,5*4); //1,1,1,1,1,1,7,8,9,10 //内存重叠,但是src>dest,出现问题了,这种src<dest得从后往前拷贝才能达到预期效果!
	
	return 0;
}

内存重叠

如下图,源src和目的dest内存有公共部分!

在这里插入图片描述

上图src>dest时,如果类似memcpy正常从左向右进行拷贝,显然结果是不对的;

这时候后memmove出现了,针对内存重叠情况做出判断,按照特定的方式(前向后 or 后向前)进行拷贝,达到预期效果;

memmove

void * memmove ( void * dest, const void * src, size_t num );



void * my_memmove ( void * dest, const void * src, size_t num )
{
     void* ret = dest;
    //核心就是根据src与desc的大小关系,进行分类操作(正拷贝 or 逆拷贝)
    if(src<dest){
        //src在desc前面!逆拷贝;
        while(num--){
            *( (char*)dest+num) =  *( (char*)src+num);//优雅~
        }
    }
    else{
        //顺拷贝;
         while(num--)
         {
        	*(char*)dest = *(char*)src;
         	dest = (char*)dest + 1;
			src = (char*)src + 1;;
    	}
    }
    return ret;
}

自定义类型(4星)

内存对齐

结构体的大小往往不是结构体中各种数据类型的加和,因为其存在内存对齐;

结构体的对齐规则:

  1. 第一个成员在与结构体变量偏移量为0的地址处。
  2. 其他成员变量要对齐到对齐数整数倍的地址处

对齐数:编译器默认的一个对齐数 **与 该成员大小的较小值 **(VS中默认的值为8 Linux中的默认值为4)

  1. 结构体总大小最大对齐数每个成员变量确定的较小对齐数最大的那个)的整数倍

    (注意,对其书不一定包含VS平台那个8,如果每个成员大小都小于8,那么结构体总大小就是那些成员中最大的类型值得整数倍)

下面是一个对齐后总大小为16的结构体:

在这里插入图片描述

为什么存在内存对齐?

性能原因:

CPU的优化规则与CPU命中率有关,大致原则是这样的:对于n字节的元素(n=2,4,8,…),它的首地址能被n整除,才能获得最好的性能。为了访问未对齐的内存,处理器需要作两次内存访问而对齐的内存访问仅需要一次访问

所以内存对齐本质上是一种空间换时间的优化;(现代内存空间大大的多,更注重时间了);

根据内存对齐的特征,设计结构体时,让较小的成员聚集在一起可以节省空间!

结构体,联合体,枚举

结构体

一个事物具有多重属性或方法,打包成一个结构体,方便操作处理;有点面向对象的意思;

struct People{
	int id;
    char* name;
    //...多重属性
 public:
    int Getid(){//方法1:返回这个人的id
        return id;
    }
    
};

枚举

枚举==列举

enum Day//星期
{
 Mon = 1,
 Tues,
 Wed,
 Thur,
 Fri,
 Sat,
 Sun
};

enum Sex//性别
{
 MALE,
 FEMALE,
 SECRET
};

{}中的内容是枚举类型的可能取值,也叫 枚举常量

这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。 例如:

枚举的优点(与宏定义常量对比一下):

宏定义的常量不够严谨;

枚举自动递增,方便管理,增加代码的可读性和可维护性;

使用方便,一次可以定义多个常量;

联合体(共用体)

联合的成员是共用同一块内存空间的(有重叠),这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有 能力保存最大的那个成员)。

 union Un
    {
        int i;
        char c;
    };
union Un un;
 un.i = 0x11223344;
 un.c = 0x55;
//下面两条输出结果一样,因为他们共用同一块空间,起始地址都是相同的
 printf("%xn", &un.i);
 printf("%xn", &un.c);

 printf("%xn", un.i);//输出11223355;  因为i和c共同一四字节的空间,第二次c放入0x55将i的44覆盖掉了;
 
	


位段

和结构体很像,与结构体相比,位段更节省空间,但是不具有跨平台性;

位段常用在确定的某些结构,省点空间,eg各种网络报文
在这里插入图片描述

struct S
{
 	char a:3;
 	char b:4;
	char c:5;
 	char d:4;
};
struct S s = {0};
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;

位段**按照类型(char int等)**开空间,如果这个空间的二进制位没用完,而且能放下下一个成员,那就共用一段空间(内存不能重叠),剩余二进制位置放不下的话,那只好再开辟一个类型空间了;

在这里插入图片描述

编译链接(3星)

我们编写的程序代码是怎样运行起来的?到底运行的是什么内容?那就是编译和链接的全过程

在这里插入图片描述

编译和链接的过程

预编译

.c生成.i文件

也叫预处理,宏替换,去掉注释,添加行号等;

编译

.i生成.s汇编文件

编译是对于预处理完的文件进行一些列的词法分析语法分析语义分析优化后产生相应的汇编代码文件,内联函数替换(一种优化)在这一阶段!

汇编

.s文件转成.o文件

汇编代码转化成机器可以执行的命令,汇编代码转换成机器指令;

前三部分用编译器 后面链接用链接器

链接

静态链接:编译阶段就把静态库就加到可执行文件中去,这样可执行文件就会比较大。

动态链接:在链接阶段仅仅只加入一些描述信息,而程序执行时再从系统中把相应动态库加载到内存中去。

链接程序的主要工作就是将有关的目标文件彼此相连接,库函数代码,我们写的多个代码文件连接起来,合并成一个可执行程序,即可运行!

条件编译

这里的“条件”就是用IF判断是否编译的时候要执行某些操作;

有点像运行时候的if判断程序怎么运行,条件编译是if判断怎么编译;

条件编译可以用于程序DE_BUG调试,也可以防止某个头文件被重复包含;

#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif //__TEST_H__ d


#pragma once //也可以防止重复包含;

操作符和关键字(4星)

volatile

volatile(易变的)是一个类型修饰符,作用是作为指令关键字,一般都是和const对应,确保本条指令不会被编译器的优化而忽略,使用这个变量时直接读取原始内存地址。

int main()
{
	volatile int i = 10;
	int a = i;

	printf("%d", i);

	//下面 汇编语句 的作用就是改变内存中i的值,但是又 不让编译器知道
	__asm
	{
		mov dword ptr[ebp - 4], 20h
	}
	int b = i;
	printf("i=%d", b);
	return 0;
}

然后,在debug(调试)版本模式运行程序,输出结果如下:

i = 10
i = 32 //修改过后的正确的i值

relese版本下的程序会自动优化,编译器不知道汇编语句改了i的值所以第二次给b赋值i的时候,看之前i没动过,就直接优化把之前i的缓存的10给b了,所以两次结果都是之前的10:

i = 10
i = 10//明明改了值,怎么还是之前的10?错误情况

这显然是有问题的,比如多线程程序改掉共用的一个变量,另个线程不知道,还用的之前的缓存cache,就有问题了;

所以volatile关键字声明这个变量是易变的换句话说,每次用这个变量的时候,都得直接读取原始内存地址,不能用任何之前的cache优化了!

const

提高程序健壮性

修饰普通变量

  1. 与宏定义常量很像;但是宏没类型检查,const更安全;
  2. const可以保护被修饰的东西,防止意外修改增强程序的健壮性
  3. const的常量一般不分配内存,直接放入编译符号表,效率更高;

修饰指针

const int* p; //指向常量的指针, p指向位置的内容不能被修改;

int * const p; //指针常量,p指向的位置不能修改;

const int* const p; //p的位置和位置里的内容都不能被修改;

修饰函数参数

void StringCopy(char *strDestination, const char *strSource指针指向的内容不被改变;);
//保证strSource“源”,指针指向的内容 在函数中 不被改变;

修饰函数返回值

针对返回引用或者指针的函数;

class Student {
public:
   //返回左值引用,可以修改;
    int& GetAge() {
        return m_age;
    }
	
    //返回右值引用,不能修改;
    const int& GetAgeConst() {
        return m_age;
    }

    void ShowAge() {
        cout << "Age: " << m_age << endl;
    }

private:
    int m_age = 0;
};

int main()
{
    Student stu;
    stu.ShowAge();

    stu.GetAge() = 5; // 会修改成员变量的值
    stu.ShowAge();

    stu.GetAgeConst() = 8; // 编译器会报错,因为该返回值被const了 成为了右值;
    stu.ShowAge();

    return 0;
}

修饰成员函数

#include <iostream>
using namespace std;
 
struct A{
private:
	int i;
public:
	void set(int n){ //set函数需要设置i的值,所以不能声明为const
		i = n;
	}
 
	int get() const{ //get函数返回i的值,不需要对i进行修改;
        			//则可以用const修饰。防止在函数体内对i进行修改。						
        			//并且修饰以后,const函数也不能调用其他的非const成员函数,提升程序的健壮性;
		return i;
	}
};

static(结合C++)

提高程序模块性;

修饰局部变量

普通的变量在函数中或者某个作用于用完以后会被释放,下次使用它的时候重新定义;

static修饰的变量就像一个全局变量一样,作用域内用完以后不会被立即释放,和全局变量一样储存在全局区,只会被定义一次;

因为是在某作用于内部,区别于全局变量在main的外部,所以极有利于模块化了;

修饰全局变量

全局变量定义在函数体外部,在全局数据区分配存储空间,且编译器会自动对其初始化。

  • 普通全局变量对整个工程可见,其他文件可以使用extern外部声明后直接使用。也就是说其他文件不能再定义一个与其相同名字的变量了(否则编译器会认为它们是同一个变量)。

  • static修饰过的全局变量只对当前文件可见其他文件不能访问,其他文件可以定义相同名字的全局变量,两者没有影响;

定义不需要共享的全局变量时,加上static修饰,那么就能有效降低各程序文件之间的耦合度避免全局变量名冲突;

C++

修饰数据成员

struct S
{
    static int a;
};
int S::a = 10;、
S s;

//S::a or s.f()这样用

当数据成员被static修饰以后,他生命周期就随类本身了,储存在全局数据区,只有这一个,不属于任何该类的实例;

修饰成员函数

struct S
{
    static int f(){ 
        //....
    };
};
int S::a = 10;
S s;

// S::f() or a.f()这样用

类似于修饰数据成员,修饰函数以后,存在全局区; 该函数不属于类的任何实例;

没有this指针!(多线程的handler函数参数就可以匹配了,不然多个this*的参数),也意味着不能访问任何数据成员了,他不属于任何实例!;

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>