目录
C++ extern关键字用法(wy游戏)
基本解释:可以置于变量或函数前,表明该变量或函数的定义在别的文件(模块)中,提示编译器遇到此变量或函数时在其他模块(文件)中寻找其定义。此外extern 也可以用来进行链接指定。(注意:只有当一个变量(函数)是一个全局变量时,extern变量才会起作用)
作用:
- 当它与"C"一起连用时,如: extern "C" void fun(int a, int b);这就告诉编译器在编译fun这个函数名时按着C的规则去翻译相应的函数名而不是C++的规则。
- 当extern不与"C"在一起修饰变量或函数时,如在头文件中: extern int g_Int; 这时候它的作用就是声明函数或全局变量的作用范围的关键字,其声明的函数和变量可以在本模块中使用,记住这是一个声明不是定义。(extern只需要指明类型和变量名即可,不能再重新赋值,初始化要在原文件进行,原文件如果不初始化,全局变量会被编译器自动初始化为0)
用法举例:
对于变量:
//文件一
#include<stdio.h>
void main()
{
//给extern的变量赋值的正确写法
extern int num;
num=1;
//给extern的变量赋值的错误写法
//extern int num = 1; //这是一个声明,不应该用来定义。
printf("%d",num);
return 0;
}
//文件二
#include<stdio.h>
int num = 5;
void func()
{
print("fun in a.c")
}
对于函数:
//文件一
#include<stdio.h>
int main()
{
extern int func(x);
func(x)
return 0;
}
//文件二
#include<stdio.h>
const int num=5;
void func(int x)
{
x=x+1
return x;
}
-
引用和指针的区别(wy游戏)
本质区别:引用是别名,指针是地址。
表现区别:
- 指针在运行时可以改变其所指向的地址(指向另一个不同的对象),而引用一旦和某个对象绑定后就不再改变。
- 从内存分配上看,程序为指针变量分配内存区域,而不为引用分配内存区域。因为引用声明必须初始化,从而指向一个已经存在的对象。引用不能指向空值。
- 从编译上看,程序在编译时分别将指针和引用添加到符号表上,符号表上记录的是变量名及变量名对应地址。指针变量在符号表上对应的地址值为指针变量自身的地址值,而引用在符号表上对应的地址值为引用对象的地址值。符号表生成后就不会再修改了,因此指针可以改变指向的对象(就是改变指针变量指向的地址),而引用对象不能改。这也是使用指针不安全,使用引用安全的主要原因。
- 使用引用的代码效率比使用指针的高,因为不存在指向空值的引用这个事实。在使用引用之前不需要测试它的合法性,而指针则应该在使用前测试,防止其为空。
- 理论上,对于指针的级别没有限制,而引用只能是一级。如下:
int** p1 //合法,指向指针的指针
int*& p2 //合法,指向指针的引用
int&* p3 //非法,指向引用的指针是非法的
int&& p4 //非法,指向引用的引用是非法的
-
函数的压栈过程 详细说明(wy游戏)
- 参数入栈:将参数从右往左依次压入系统栈中。
- 返回地址入栈:当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继续执行。
- 代码区跳转:处理器从当前代码区跳转到被调用函数的入口处,
- 栈帧调整,具体包括:
①保存当前栈帧状态值,已备后面恢复本栈帧时使用(EBP入栈)
②将当前栈帧切换到新栈帧(将当前ESP值装入EBP,更新栈帧底部)
③给新栈帧分配空间(把ESP减去所需要空间的大小,抬高栈顶)
④对于_stdcall调用约定,函数调用时用到的指定序列大致如下(背下面的本题基本就ok了):
push 参数3 ;假设该函数有3个参数,将参数从右往左依次入栈
push 参数2
push 参数1
call 函数地址;call指令将同时完成两项工作:a)向栈中压入当前指令地址的下一个指令地址,即保存返回地址。b)跳转到所调用函 数的入口处。
push ebp ;保存旧栈帧的底部
mov ebp,esp ;设置新栈帧底部(栈帧切换)
sub esp,xxx ;设置新栈帧的顶部(抬高栈顶,为新栈帧开辟空间)
EBP:基址指针寄存器,其内存放着一个指针,该指针永远指向系统栈最上面的一个栈帧的底部。
ESP:栈指针寄存器,其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。
函数返回看这个地址:https://blog.youkuaiyun.com/u011555996/article/details/70211315
-
内存的不同用途(扩充知识点)
根据不同的操作系统,一个进程可能被分配到不同的内存区域去执行。但是不管什么样的操作系统,什么样的计算机架构,进程使用的内存都可以按照功能大致分为以下四个部分:
- 代码区:这个区域存储着被装入执行的二进制机器代码,处理器会到这个区域取指并执行
- 数据区:用于存储全局变量等。
- 堆区:进程可以在堆区动态地请求一定大小的内存,并在使用完之后归还给堆区。动态分配和回收是堆区的特点。
- 栈区:用于动态地存储函数之间的关系,以保证被调用函数在返回时恢复到母函数中继续执行。
-
内联函数的相关概念(扩充知识点)
内联函数(inline)定义: 当函数被声明为内联函数之后, 编译器会将其内联展开, 而不是按通常的函数调用机制进行调用。内联函数是C++为提高程序运行速度进行的改进,是一种以空间换时间的做饭。
使用条件:在使用内联函数时,如果调用函数的时间比处理函数的时间长,节省的时间占大头。但如果调用时间小于处理函数的时间短,则没有必要使用内联函数。经验之谈,尽量不要内联超过 10 行的函数。
用法举例:
int max(int a, int b)
{
return a > b ? a : b;
}
为这么一个小的操作定义一个函数的好处有:
① 阅读和理解函数 max 的调用,要比读一条等价的条件表达式并解释它的含义要容易得多
② 如果需要做任何修改,修改函数要比找出并修改每一处等价表达式容易得多
③ 使用函数可以确保统一的行为,每个测试都保证以相同的方式实现
④ 函数可以重用,不必为其他应用程序重写代码
虽然有这么多好处,但是写成函数有一个潜在的缺点:调用函数比求解等价表达式要慢得多。在大多数的机器上,调用函数都要做很多工作:调用前要先保存寄存器,并在返回时恢复,复制实参,程序还必须转向一个新位置执行
C++中支持内联函数,其目的是为了提高函数的执行效率,用关键字 inline 放在函数定义(注意是定义而非声明,下文继续讲到)的前面即可将函数指定为内联函数,内联函数通常就是将它在程序中的每个调用点上“内联地”展开,假设我们将 max 定义为内联函数:
inline int max(int a, int b)
{
return a > b ? a : b;
}
则调用: cout<<max(a, b)<<endl;
在编译时展开为: cout<<(a > b ? a : b)<<endl;
从而消除了把 max写成函数的额外执行开销
-
内联函数和常规函数的区别(扩充知识点)
常规函数:运行程序时,常规函数调用会使程序跳到另一个地址(函数地址),并在函数结束时返回。
详细过程:执行到函数调用指令时,程序将在函数调用后立即存储该指令的内存地址(或该指令的下一条指令的内存地址,具体是哪个内存地址可以自己运行试一下),并将函数参数压入堆栈(为此保存的内存块),跳到标记函数起点的内存单元,执行函数代码(也许还需要将返回值放入寄存器中),然后跳回到地址被保存的指令处。来回跳跃并记录跳跃位置意味着调用函数时,需要一定的开销。
内联函数:内联函数的编译代码与其他程序代码“内联”起来了。也就是说,编译器将使用相同的函数代码替换函数调用。
对于内联函数,程序无需跳转到代码区另一个位置处执行代码,再跳回来。因此,内联函数的运行速度比常规函数快,但代价是需要占用更多的内存。(在使用内联函数时,如果调用函数的时间比处理函数的时间长,则节省的时间占大头。但如果调用时间小于处理函数的时间短,则没必要使用内联函数)
区别:
- 常规函数调用会使程序跳到另一个地址,而内联函数调用程序不会。
- 内联函数比常规函数运行速度快,但占用内存更多。
- 内联函数使用和常规函数定义不一样(内联函数需要在定义函数的函数名前加 “inline“)。
-
内联函数的优缺点(wy游戏)
优点: 当函数体比较小的时候, 内联该函数可以令目标代码更加高效(对于存取函数以及其它函数体比较短, 性能关键的函数, 鼓励使用内联)。
缺点: 滥用内联将导致程序变慢,毕竟内联是用空间换时间的。 内联可能使目标代码量或增或减, 这取决于内联函数的大小。内联非常短小的存取函数通常会减少代码大小, 但内联一个相当大的函数将戏剧性的增加代码大小(现代处理器由于更好的利用了指令缓存, 小巧的代码往往执行更快)。
-
虚函数的底层机制(wy游戏)
首先要说明,多态的实现是基于虚函数的,而虚函数是通过虚函数表来实现C++多态。
底层机制:
- 每一个含有虚函数的对象中,都含有一个指针VPTR,指向虚函数表。
- 虚函数表里存放都是指针(指向虚函数地址的指针)。(虚函数表可以看成是存放指针的数组)
- 对于派生类对象覆盖的虚函数,对象的虚函数表里的指针指向的是这个覆盖的虚函数的地址。而对于派生类对象没有覆盖的虚函数,这个对象的虚函数表中,其指针指向的是基类虚函数的地址。
举例:
一般继承:
#include<iostream>
using namespace std;
class Base
{
public:
virtual void fun1 () {cout<<" printf base fun1!" <<endl;}
virtual void fun2 () {cout<<" printf base fun2!" <<endl;}
private:
int m_data1;
} ;
class Derive: public Base
{
public:
void fun1 () {cout<<" printf derive fun1!" <<endl;}
void fun3 () {cout<<" printf derive fun3" <<endl;}
private:
int m_data2;
} ;
int main ()
{
Base *pBase=new Derive;
Derive a;
pBase->fun1 () ;
pBase->fun2 () ;
a.fun3 () ;
return 0;
}
在每个含有虚函数的类对象中,都有一个VPTR,指向虚函数。
派生类也会继承基类的虚函数,如果在派生类中改写虚函数,虚函数表就会受到影响;表中元素所指向的地址不是基类的地址,而是派生类的函数地址。
当执行语句pBase->fun1()时,由于PBase指向的是派生类对象,于是就调用的Deriver::fun1()。
多重继承:
#include<iostream_h>
class base1
{
public:
virtual void vn(){}
private:
int i ;
);
class base2
{
public:
virtual void vf2(){}
private:
int j;
);
class derived:public base 1,public base2
{
public:
virtual void vf3(){}
private:
int k:
);
void main()
{
derived d:
base1 pl;
base2 p2;
pl=&d;p2 &d:
pl->vfl();
p2->vf2();
}
如果一个类具有多个包含虚函数的父类,编译器会为它创建多个VIrtual table,每个virtual table中各个虚函数的顺序与相应的父类一样。
(其实这就可以说理解为,继承两个父类的子类,有两个指向虚函数表的指针VPTR,故子类的大小会加上两个指针的大小(2*8字节=16字节))