常见简单问题:
1、局部变量存放在哪里??
stack(栈)上
int a = 10;
此时的a就是存放在栈上的,汇编指令就是mov dword ptr[ebp-4], 0Ah,以前有人这样讲,分配了一个4字节的内存,取名为a,其实这样是不准确的,实际上就没有叫a名字的内存地址,都是汇编指令来做的,指令存放在.text段,指令做的事情是在stack上ebp-4位置存放一个数字10,它不属于数据的一部分,属于指令的内容,这就是真相,而且,a在符号表中不产生符号 ( 数据段 = .data + .bss ) malloc和new出来的都是在堆上(heap)
可以进行扩充,全局变量存放在哪里,static修饰的变量又存放在哪里??,可以看看我的虚拟地址空间博客
2、拷贝构造函数,为什么参数总是一个同类型的一个引用,能不能不用引用,换成同类型的对象??
class Test
{
public:
Test(const Test & src)
{
}
}
Test t1;
Test t2(t1);
如上代码,先是调用默认的构造函数构了一个对象 t1,第二行代码是想要调用拷贝构造函数,拷贝构造出来一个新的对象 t2, 其实是t2.Test(t1),这样调用的拷贝构造函数,然后src引用了t1,以后 src 就是 t1,没有产生新的形参对象,没有新内存的分配,但是如果形参处改变成Test src,那么当调用拷贝构造函数的时候,由于形参不是引用了,就需要产生一个形参对象,实参到形参是一个初始化的过程,那么就会有 t1 拷贝构造形参对象src,由于要拷贝构造src,得去调用拷贝构造函数,又产生了相同的问题,有矛盾了,但是读者不要以为这个就是递归,无限的循环下去了,这个错误是在编译阶段就被编译器发现的,是语法错误,程序根本不会运行起来的,所以也不会发生你想象的 “死循环”(知道真相的你,眼泪掉下来)
3、内联函数和普通函数的区别??
核心问题就是函数调用的开销(栈帧的开辟释放)
我们知道,一般的函数调用,都是这样的,先进行栈帧的开辟,第一步push ebp,然后开辟栈帧空间 mov ebp,esp, sub esp,4Ch,栈帧开辟完成以后,根据不同的编译器,可能还会对栈帧进行初始化操作,Windows是进行初始化的,但是 gcc 和 g++ 不会对栈帧进行初始化的,Windows上初始化,汇编代码是 res stos, 统一初始化成了0xcccccccc,所以你打印一个没有初始化的局部变量的时候,打印的是-858993460。它在内存上的十六进制表示就是0xcccccccc
栈帧的回退(释放)
mov esp, ebp
pop ebp
ret
ret 做了两件事情:(对应的call也做了两件事情),我的博客 函数调用过程也说过了
① 把当初保存的EIP的值出栈
② 放入PC寄存器中
inline函数,不是写了关键字inline,就一定可以内联的,看具体情况,如果函数中有循环不可以内联,递归函数也不可以内联,因为它被调用多少次是不知道的,编译器怎么知道,该把函数代码展开多少次合适呢
而且lnline函数,在符号表中是不产生符号的,内联函数一旦内联成功是没有办法调试的,很多人说,我编写了一个内联函数,我怎么还可以调试呢,这是因为,你是在Debug版本下,如果换成是release版本就不可以了,同样也可以看看我其他的博客,也具体说到了内联函数还有普通函数,这里只是简答问题
4、如何实现一个不可以被继承的类??
考查的是派生类的构造过程
先是 基类构造,然后 派生类构造
所以,如果我们把基类的构造函数私有化,那么就完成了,因为在类的三种继承方式中,基类的私有成员,虽然都是可以被派生类继承去的,但是不可访问的,当派生类非要继承基类的话,派生类对象是无法构造的
5、什么是纯虚函数,为什么要有纯虚函数???
形如 vritual void func() = 0;就是纯虚函数
有纯虚函数的类,叫做抽象类,抽象类是无法实例化对象的,但是可以定义指针或者引用
发生在继承中,纯虚函数一般定义在基类中,基类不代表任何的实体,它的主要作用之一就是给所有的派生类保留统一的纯虚函数接口,让派生类重写,方便使用多态机制(动多态),用基类指针指向派生类对象,访问派生类的同名覆盖方法,指向那个派生类,就可以调用那个同名覆盖方法。因为基类不需要实例化,所以它的方法也就不知道该如何去实现
虚函数表是在编译阶段产生的,如果一个类有虚函数,那么在编译阶段就会给这个类产生一个虚函数表,运行时,虚函数表加载到.rodata段,所有该类定义的对象,虚函数指针指向的是同一张同类型的虚函数表
6、说一下C++中的const const 和 static的区别
C++中,const 定义的叫做常量,编译方式比较特殊,是在编译过程中,把出现常量名字的地方,替换成常量的值
int a = 10;
int *p = (int*)&a;
*p = 20;
cout << a << *p << endl;
输出的结果是 10 和 20
*p = 20确实把内存中的值修改成了20
但是这个a在编译过程中,已经被10个替换掉了,所以这个和 a 没有什么关系
这就是const定义的常量,但是它也可以退化成常变量
int b = 10;
const int a = b;
初值不明确
这里的 a 就无法被替换了,因为它的初值是一个变量,并不是用一个常量数字,此时它就叫做常变量,在c语言中,一直都是常变量
const还可以定义常成员方法,主要的变化就是,它的this指针,从Test * this 变成了 const Test* this,这样的话,普通对象和常对象就都可以调用了,只可以进行读操作,不可以修改
const 和 static的区别
从面向过程来说:
const :全局变量,局部变量,形参变量 static: 全局变量 ,局部变量
const :不能修饰函数 static可以修饰函数,使得只能在本文件可见(符号表,从g变成l)
从面向对象来说:
const 修饰的是常方法 / 成员变量
常成员变量是不能被修改的变量,必须在构造函数的初始化列表中进行初始化
常方法是普通对象和常对象都可以调用的,但是只能读,不能写,依赖对象
static 静态方法 / 成员变量
静态成员变量,是所有对象共享的,不依赖于对象,依赖于类
静态成员方法不产生this指针了,不依赖于对象的,依赖于类
7、四种类型转换
const_cast 去掉常量属性
static_cast 类型安全的转换,编译器认为不安全,就不会进行类型转换了
比如类型不同的指针之间的转换是不允许的,它认为是不安全的,在C语言中,任何类型的指针都可以随意转换,static_cast只是把编译器认为类型安全的进行转换,类型不安全的就直接报错了
reinterpret_cast C风格的类型转换,没有安全性可言,随意转换
dynamic_cast 支持RTTI类型信息识别的类型转换
当把一个基类指针转换成一个相应派生类类型的指针的时候,它会识别这个基类指针到底是否指向的是相应派生类的一个对象
RTTI(Run-Time Type Identification),通过运行时类型信息程序能够使用基类的指针或引用来检查这些指针或引用所指的对象的实际派生类型
RTTI 提供了以下两个非常有用的操作符:
typeid 操作符,返回指针和引用所指的实际类型
dynamic_cast 操作符,将基类类型的指针或引用安全地转换为派生类型的指针或引用