内存对齐
- 为什么要有内存对齐
- 1、平台原因:不是所有的硬件平台都可以随意的访问某个内存地址上的数据的,有些硬件平台只能访问某些特定地址上的某些特定类型的数据,否则会抛出异常。
- 2、性能原因:为了访问没有对齐的内存空间时,操作系统可能要对这块内存空间进行两次或者多次内存访问。但是对于对齐的内存空间,操作系统只需要进行一次访问就行
- 内存对齐的原则
- 1、第一个成员在结构体偏移量为0的地址处
- 2、结构体中其他成员要对齐到对齐数的整数倍处,(对齐数:编译器的默认对齐数和成员自身大小的较小值),(VS默认是8字节,Linux默认是4字节)。
- 3、结构体总大小为最大对齐数的整数倍。(最大对齐数:每个成员的对齐数的最大值)
- 4、如果结构体中有嵌套,嵌套的结构体对齐到自己最大对齐数的整数倍。整个结构体对齐到最大对齐数(含嵌套的结构体的对齐数)的整数倍。
- 评价:结构体对齐是拿空间换时间的做法。那么我们要做到既节省空间,系能也要好,那么我们尽量在设计结构体的时候讲占用空间小的成员集中在一起。
大小端
- 大端:数据的低位存在内存的高地址处,数据的高位存在内存的低地址处。
- 小端:数据的低位存在内存的低地址处,数据的高位存在内存的高地址处。
- 一张图让你彻底明白
进程的地址空间
- 栈的大小:
windows下:虚拟内存中分配的大小是1M,物理内存中默认
分配4k,可以被修改。
linux:linux下用ulimit查看的话是10M,可以被修改。 - 堆的大小:
虚拟内存中堆分配的大小是1M,物理内存中是4k,可以被修
改
柔性数组
- 柔性数组概念:结构体中的最后一个成员可以是一个不指定大小的数组,成
为柔性数组。柔性数组前面必须还要有其他成员,可以动态的为柔性数组分配大小。结构体的大小不计入柔性数组的大小。 - 可以参考一下这篇博文:https://www.cnblogs.com/pluviophile/p/7571410.html
函数指针
- 指向函数的指针就叫做函数指针。函数指针初始化的时候直接给指针赋值函数名,或者赋值为取地址函数名都是可以的。
- 用途:
- 回调函数:将一个函数指针作为参数传递给其他函数。
- 转移表:实际上是一个函数指针数组,就是一个数组里面放的全部都是函数指针。
- 我来写一个函数指针
void test()
{
printf("hello world!\n");
}
void (*pTest)(); // pTest 就是一个函数指针
pTest = test ;
void (*ppTest)() = test; //也可以在声明的时候就进行初始化
- 上面的代码 pTest首先与 * 结合,表明他是一个指针,接下来他的返回值是void(没有返回值),他的参数为空,代表不用传参。
程序生成的过程,编译链接过程
- 编译链接过程分为预处理,编译,汇编,链接四个过程。
- 预处理:消除注释,替换宏,处理预处理指令,包含库文件
- 编译:词法分析,语法分析,语义分析,优化处理
- 汇编:把汇编语言翻译成目标机器指令,生成目标文件
- 链接:将目标文件链接生成可加载,可执行的目标文件
宏的优缺点
- 优点:
- 提高了程序的可读性,同时也方便进行修改;
- 提高程序的运行效率:使用带参的宏定义既可完成函数调用的功能,又能避免函数的出栈与入栈操作,减少系统开销,提高运行效率;
- 宏是由预处理器处理的,通过字符串操作可以完成很多编译器无法实现的功能。
- 缺点:
-
由于是直接嵌入的,所以代码可能相对多一点;
-
嵌套定义过多可能会影响程序的可读性,而且很容易出错;
-
对带参的宏而言,由于是直接替换,并不会检查参数是否合法,存在安全隐患。
-
预编译语句仅仅是简单的值代替,缺乏类型的检测机制。这样预处理语句就不能享受C++严格的类型检查的好处,从而可能成为引发一系列错误的隐患。
宏与函数的区别:
- 时间上考虑:
- 宏只占编译时间,函数调用则占用运行时间(分配单元,保存现场,值传递,返回),每次执行都要载入,所以执行相对宏会较慢。
- 使用宏次数多时,宏展开后源程序很长,因为每展开一次都使程序增长,但是执行起来比较快一点(这也不是绝对的,当有很多宏展开,目标文件很大,执行的时候运行时系统换页频繁,效率就会低下)。而函数调用不使源程序变长。 2. 使用宏次数多时,宏展开后源程序很长,因为每展开一次都使程序增长,但是执行起来比较快一点(这也不是绝对的,当有很多宏展开,目标文件很大,执行的时候运行时系统换页频繁,效率就会低下)。而函数调用不使源程序变长。
- 安全性考虑:
3. 函数调用时,先求出实参表达式的值,然后带入形参。而使用带参的宏只是进行简单的字符替换。
4. 函数调用是在程序运行时处理的,分配临时的内存单元;而宏展开则是在编译时进行的,在展开时并不分配内存单元,不进行值的传递处理,也没有“返回值”的概念。
5. 对函数中的实参和形参都要定义类型,二者的类型要求一致,如不一致,应进行类型转换;而宏不存在类型问题,宏名无类型,它的参数也无类型,只是一个符号代表,展开时带入指定的字符即可。宏定义时,字符串可以是任何类型的数据。
6. 宏的定义很容易产生二义性,如:定义#define S(a) (a)(a),代码S(a++),宏展开变成(a++)(a++)这个大家都知道,在不同编译环境下会有不同结果。 - 结构性考虑:
7. 调用函数只可得到一个返回值,且有返回类型,而宏没有返回值和返回类型,但是用宏可以设法得到几个结果。
8. 函数体内有Bug,可以在函数体内打断点调试。如果宏体内有Bug,那么在执行的时候是不能对宏调试的,即不能深入到宏内部。
9. C++中宏不能访问对象的私有成员,但是成员函数就可以。
宏与内联函数的区别
- 内联函数和宏的区别在于,宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。你可以象调用函数一样来调用内联函数,而不必担心会产生于处理宏的一些问题。
- 内联函数也有一定的局限性。就是函数中的执行代码不能太多了,如果,内联函数的函数体过大,一般的编译器会放弃内联方式,而采用普通的方式调用函数。这样,内联函数就和普通函数执行效率一样了。
- 如何选择使用宏还是函数:
以下情况可以选择宏,其他情况最好选用函数- 一般来说,用宏来代表简短的表达式比较合适。
- 在考虑效率的时候,可以考虑使用宏,或者内联函数。
- 在头文件保护(防止重复包含编译),条件编译中的#ifdef,#if defined以及assert的实现。
#ifndef和#pragma有什么区别
#pragma once
是微软独有,不支持跨平台,且它只要发现头文件被包含就不会打开头文件。#ifndef
是要每次打开文件判断头文件是否被包含的。所以总的说#pragma
编译的效率更快。
自己实现一个 atoi 函数
#include<iostream>
#include<string>
#include<vector>
bool status = true; //状态,出现错误一律是false,正确就是true
int my_atoi(std::string str)
{
//如果遇到字符不是数字字符、或者字符前有空格都返回错误
//如果输入的字符串中没有数字,也给他返回错误
//如果要转换的字符串过大,可能会溢出,溢出的话也给他返回错误
int flag = 1; //标记正负, -1 代表负数 1 代表正数
int i = 0; //遍历str的下标
std::vector<int> vec;
if (str.empty())
{
status = false;
return 0;
}
if (!((str[0] >= '0' && str[0] <= '9') || (str[0] == '+') || (str[0] == '-')))
{
status = false;
return 0;
}
if (str[0] == '+')
{
flag = 1;
i = 1;
}
else if (str[0] == '-')
{
flag = -1;
i = 1;
}
for (; i < str.size(); ++i)
{
if (!(str[i] >= '0' && str[i] <= '9'))
{
status = false;
return 0;
}
else
{
vec.push_back(str[i] - '0');
}
}
int len = vec.size() - 1;
int num = 1;
long int res = 0;
for (int i = len; i >= 0; --i)
{
res += vec[i] * num;
num *= 10;
}
res *= flag;
if (res >= INT_MAX || res <= INT_MIN)
{
status = false;
return 0;
}
return (int)res;
}
int main()
{
std::string str;
std::cin >> str;
int res = my_atoi(str);
if (status == false)
{
std::cout << "输出有误,请重新输入" << std::endl;
return 0;
}
std::cout << res << std::endl;
return 0;
}