C++常见面试题总结1---C++指针
指针变量所存的内容就是内存的地址编号。
&i:返回i变量的地址编号。*p:指针p所指向的地址的内容。
无效指针、野指针、悬空指针
无效指针: 指针变量的值是NULL,或者未知的地址值,或者是当前应用程序不可访问的地址值,这样的指针就是无效指针,不能对他们做解指针操作,否则程序会出现运行时错误,导致程序意外终止。未经初始化的指针就是个无效指针,所以在定义指针变量的时候一定要进行初始化。如果实在是不知道指针的指向,则使用nullptr进行赋值。
野指针: 就是没有被初始化过的指针。指针变量未及时初始化 => 定义指针变量及时初始化,要么置空。
悬空指针: 是最初指向的内存已经被释放的指针。指针free或delete之后没有及时置空。因此使用时注意:释放操作后应立即置空,防止出现悬空指针。
void* 指针
void* 指针是一种特殊的指针类型,可用于存放任意对象的地址,但是丢失了类型信息。如果想要完整的提取指向的数据,程序员就必须对这个指针做出正确的类型转换,然后再解指针。编译器不允许直接对void*类型的指针做解指针操作(提示非法的间接寻址)。
函数指针
在程序载入到内存后,函数的机器指令存放在一个特定的逻辑区域:代码区。既然是存放在内存中,那么函数也是有自己的指针的。函数名单独使用时就是函数指针。
函数指针的声明: int (*function)(int,int);
函数指针用处:
- 函数指针和虚函数有着一样的用处(其实本质一样),都是为了:延时绑定(晚绑定)。
- 回调函数,让一个函数可以作为另一个函数的参数,即回调函数。
注意:不要把非静态局部变量的地址返回。局部变量是在栈中的,由系统创建和销毁,返回之后的地址有可能有效也有可能无效,这样会造成bug。可以返回全局变量、静态的局部变量、动态内存等的地址返回。
函数指针: 指向函数的指针;
指针函数: 返回指针的函数。
每个类编译后,是否创建一个类中函数表保存函数指针,以便用来调用函数?
普通的类函数(不论是成员函数,还是静态函数)都不会创建一个函数表来保存函数指针。只有虚函数才会被放到函数表中。但是,即使是虚函数,如果编译器能明确知道调用的是哪个函数,编译器就不会通过函数表中的指针来间接调用,而是会直接调用该函数。
测试1:
int* p[5]; //指针数组,数组大小为10,且每个元素都是int指针
int(*p)[5]; //数组指针,p指向一个int类型的数组
int* p(int); //函数声明,函数名是p,参数是int类型,返回值时int*类型
int(*p)(int); //函数指针,该指针指向的函数的参数和返回值都是int类型
测试2: 定义一个宏实现求结构体成员的偏移量(地址的偏移量),怎么做?
#define get_offset(struct_name, struct_member) (unsigned int)(&(((struct_name *)(0))->struct_member))
结构体元素的偏移量是针对首地址的,因此,第一步就是确定当前结构体的地址。(struct_name *)(0) 是常数0强制转换为struct_name类型的指针的类型转换符,这样该指针就指向了结构体的首地址(为0)。然后通过该指针指向不同的成员,并取得不同成员的地址进行显示转换,最终得到各个成员的地址偏移量。
访问这个结构体的某一个成员变量,这个部分是非法的,因为相当于是访问一个空指针,但是如果再在前面加上&取地址符号,就是合法的(访问非法,取地址是可以的)。
测试3: 一个子类继承了父类,如何获得子类对象的地址?
&((A*)0)->i;
当编译器要用要一个成员变量的时候,它会根据对象的首地址加上成员的偏移量得到成员变量的地址。当对象的首地址为0时,得到的成员变量地址就是它的偏移量。没有为A的对象分配内存,那怎么可以得到它的地址呢?这里确实没有分配内存,但是这个例子并没有要求有内存,也不对内存进行操作,所以不会引来崩溃。
this指针
this指针的本质是一个隐式插入类成员函数体中,用来指向需要操作的对象的指针常量。
C ++为成员函数提供了一个名字为this的指针,这个指针称为自引用指针,可以应用于所有类成员函数。每当创建一个对象时,系统就把this指针初始化为指向该对象,即this指针的值是当前被调用的成员函数所在的对象的起始地址。不同的对象调用同一个成员函数时,C++编译器将根据成员函数的this指针所指向的对象来确定应该引用哪一个对象的数据成员。
this指针的特性:
- 指向当前对象,可用于所有的类成员函数(只能在成员函数中使用,全局/静态函数不能使用this(静态函数不属于固定对象)),但不能用于初始化列表;
- this以隐含参数的形式传入,而非成员的一部分,所以不会影响sizeof(obj)的大小;
- this指针是不能更改指向的(它指向的内容可以修改),即它是const类型修饰的。
- this指针在成员函数的开始前构造,在成员函数的结束后清除。这个生命周期同任何一个函数的参数是一样的,没有任何区别。
- this指针会因编译器不同而有不同的放置位置。可能是栈,也可能是寄存器,甚至全局变量。
this指针的好处:
避免形参和数据成员重名,即用this指针来区分;在类的非静态成员函数中返回对象本身,可使用return *this;可以实现链式表达。
在成员函数里delete this会怎么样(对象在栈上,在堆上)?
可以delete this,但是一定要保证this对象是new出来的,不是在栈空间分配的,也不是在全局空间分配的,也不能是new[]分配的。而且,在delete之后不能访问该对象的数据成员和成员函数。
delete操作一般是先调用析构函数,再调用delete运算符。而且delete之后,该内存不会立刻被释放,只是做个标记,告诉操作系统这块内存可以被释放掉了。至于系统什么时候会释放是不知道的。所以delete this指针本身没问题,只是不能在delete this之后,访问对象的成员变量以及调用虚函数,因为成员变量和vptr是存放在该内存块的,如果以后再去访问,就是访问已经被销毁的内存。
如果在类的析构函数中调用delete this,会发生什么?
会导致堆栈溢出。原因很简单,delete的本质是“为将被释放的内存调用一个或多个析构函数,然后,释放内存” (来自effective c++)。显然,delete this会去调用本对象的析构函数,而析构函数中又调用delete this,形成无限递归,造成堆栈溢出,系统崩溃。
const与指针
指针常量和常量指针,两者的区别是看const修饰的谁。
指针常量: 是指针本身是常量,换句话说,就是指针里面所存储的内容(内存地址)是常量,不能改变。但是,内存地址所对应的内容是可以通过指针改变的。以 * 为分界线,其左侧表示指针指向的类型,右侧表示指针本身的性质。
int a = 97;
int b = 98;
int* const p = &a;
*p = 98; //正确
p = &b; //编译出错
常量指针: 是指向常量的指针,换句话说,就是指针指向的是常量,它指向的内容不能发生改变,不能通过指针来修改它指向的内容。但是,指针自身不是常量,它自身的值可以改变,从而指向另一个常量。const 的位置在指针声明运算符 * 的左侧。只要 const 位于 * 的左侧,无论它在类型名的左边或右边,都表示指向常量的指针。
int a = 97;
int b = 98;
const int* p = &a;
*p = 98; //编译出错
p = &b; //正确
用指针有什么好处?
- 在数据传递时,如果数据块较大(如数据缓冲区或比较大的结构),这时就可以使用指针传递地址而不是实际数据,即提高传输速度,又节省大量内存。
- 在数据结构中,链表、树、图等大量的应用都离不开指针。
- 动态分配内存必须要用到指针。
- 快速访问类的成员数据和函数。
- 更容易实现函数的编写和调用。
- 函数参数是传递信息的基本方法,外部传入内部用值传递,内部传入外部用指针。可以直接在内存中修改传入参数的值,减少内存消耗,提升程序效率。
指针使用过程中有哪些注意事项?
- 要避免使用未初始化的指针。很多运行时错误都是由未初始化的指针导致的,而且这种错误又不能被编译器检查所以很难被发现。解决办法是:在定义指针时候进行初始化。初始化或赋值时必须保证类型匹配。
- 不能将两个指针变量指向同一块动态内存。如果其中一个生命期结束释放了该动态内存,另一个指针所指向的地址虽然被释放了但该指针并不等于NULL,这就是所谓的悬空指针错误,这种错误很难被察觉,而且非常严重,因为这时该指针的值是随机的,可能指向一个系统内存而导致程序崩溃。
- delete释放指针所指向的内存后要将该指针置空,否则会产生悬空指针。
- 为一个指针再次分配内存之前一定要保证它原先没有指向其他内存,防止出现内存泄漏。解决的办法是:必须判断该指针是否为空,因为如果释放某内存后相应指针不置空的话就不能为其分配新内存。
指针和引用的区别?
- 指针可以初始化为NULL;引用必须初始化且必须是已有对象的引用;
- 指针有自己的一块空间;而引用只是一个别名;
- 作为参数传递时,指针需要被解引用才可以对对象进行操作;而直接对引用的修改都会改变引用所指向的对象;
- 指针可以指向其它对象;但引用只能是一个对象的引用,不能改变;
- 返回动态内存分配的对象或内存必须用指针;用引用可能引起内存泄露;(对象通过指针获取申请的堆上内存,指针是指向动态内存区域的唯一方式。而引用实质是对象的一个别名,对象被析构之后,引用将会失效,所以可能会使得堆上的内存空间没有及时释放,造成内存泄露。)
- 可以有多级指针(**p);没有多级引用,即引用只有一级;
- 使用sizeof看一个指针的大小是4(sizeof计算指针的内存空间,在32位机上是4B,在64位机上是8B),而引用则是被引用对象的大小。
引用常量不存在,没有int& const p。而常量引用是存在的cosnt int &p。
什么时候需要使用常引用?
如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用。引用型参数应该在能被定义为const的情况下,尽量定义为const 。常引用声明方式:const 类型标识符 &引用名=目标变量名;
引用的底层也是指针实现的,内置类型指针传递和引用传递的汇编代码是一样的,那C++为什么还需要引入引用呢?
- C++引入引用机制主要是为了支持运算符重载。
- 用指针经常犯的错:1,操作空指针;2,操作野指针;3,不知不觉改变了指针的值,而后还以为该指针正常。如果我们要正确的使用指针,我们需要人为地保证这三个条件。而引用的提出就解决了这个问题。
- 引用区别于指针的特性是 :1,不存在空引用(保证不操作空指针);2,必须初始化(保证不是野指针);3,一个引用永远指向他初始化的那个对象(保证指针值不变)。人为保证变为了编译器来保证,更加安全。
用户自定义的类型最好用引用传参,这样可以避免不必要的构造函数和析构函数调用,但是对于内置类型,按值传参会比按引用传参更高效。
既然C++引入了引用,那为什么C++不和Java一样抛弃指针呢?
- 指针是C/C++重要特性之一,这种特性可以直接操作硬件;Java不可以。
- C++的指针继承于C,若要移除指针,会造成严重的兼容性问题。
- 指针能够大幅度提高程序的效率;而没有指针的Java的效率相对较低。
C++智能指针
为什么要使用智能指针?
智能指针的作用是管理一个指针, 因为存在以下这种情况: 申请的空间在函数结束时忘记释放, 造成内存泄漏。 使用智能指针可以很大程度上的避免这个问题, 因为智能指针就是一个类,当超出了类的作用域是, 类会自动调用析构函数, 析构函数会自动释放资源。 所以智能指针的作用原理就是在函数结束时自动释放内存空间, 不需要手动释放内存空间。
四种智能指针:auto_ptr, shared_ptr, weak_ptr, unique_ptr,其中C++11 支持后三个,第一个已经被C++11弃用。
(1)auto_ptr
采用所有权模式。
auto_ptr<string> p1 (new string ("I reigned lonely as a cloud.” ));
auto_ptr<string> p2;
p2 = p1; //auto_ptr 不会报错
p2 剥夺了 p1 的所有权,此时不会报错,但是当程序运行时访问 p1 将会报错。 所以 auto_ptr的缺点是: 存在潜在的内存崩溃问题!
(2)unique_ptr
unique_ptr 实现独占式拥有或严格拥有概念, 保证同一时间内只有一个智能指针可以指向该对象。 它对于避免资源泄露(例如“以 new 创建对象后因为发生异常而忘记调用 delete” )特别有用。unique_ptr 比 auto_ptr更安全。
unique_ptr<string> p1 (new string ("I reigned lonely as a cloud.” ));
unique_ptr<string> p2;
p2 = p1; //unique_ptr会报错
可以使用std::move()将一个 unique_ptr 赋给另一个。
(3)shared_ptr
shared_ptr 实现共享式拥有概念。 多个智能指针可以指向相同对象, 该对象和其相关资源会在“最后一个引用被销毁” 时候释放。 从名字 share 就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。 可以通过成员函数 use_count()来查看资源的所有者个数。 除了可以通过 new 来构造, 还可以通过传入 auto_ptr, unique_ptr,weak_ptr 来构造。当我们调用 release()时, 当前指针会释放资源所有权, 计数减一。 当计数等于 0 时, 资源会被释放。
shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。
#include<iostream>
using namespace std;
template <class T>
class SmartPtr
{
private:
T* ptr; //底层真实指针
int* use_count; //被多少指针引用,声明成指针是为了方便对其的递增或递减操作
public:
SmartPtr(T* p); //SmartPtr<int>p (new int(2));
SmartPtr(const SmartPtr<T>& orig); //SmartPtr<int> q(p);
SmartPtr<T>& operator=(const SmartPtr<T>& rhs); //q = p
~SmartPtr();
T operator*(); //为了能把智能指针当成普通指针操作,定义解引用操作
T* operator->(); //定义取成员操作
T* operator+(int i); //定义指针加一个常数
//int operator-(SmartPtr<T>& t1, SmartPtr<T>& t2);
int getcount();
};
template <class T>
int SmartPtr<T>::getcount()
{
return *use_count;
}
//template <class T>
//int SmartPtr<T>::operator-(SmartPtr<T>& t1, SmartPtr<T>& t2)
//{
// return t1.ptr - t2.ptr;
//}
template <class T>
SmartPtr<T>::SmartPtr(T* p)
{
ptr = p;
try
{
use_count = new int(1);
}
catch (...)
{
delete ptr; //申请失败时释放真实指针和引用技术的内存
ptr = nullptr;
delete use_count;
use_count = nullptr;
cout << "Allocate memory for use_count fails.\n";
exit(1);
}
cout << "Constructor is called!\n";
}
template <class T>
SmartPtr<T>::SmartPtr(const SmartPtr<T>& orig)
{
//引用计数保存在一块内存,所有SmartPtr对象的引用计数都指向这里
use_count = orig.use_count;
ptr = orig.ptr;
++(*use_count); //当前对象的引用计数加1
cout << "Copy constructor is called!\n";
}
template <class T>
SmartPtr<T>& SmartPtr<T>::operator=(const SmartPtr<T>& rhs)
{
//SmartPtr<int> p,q; p = q;
//首先给q指向的对象的引用计数加1,然后给p原来指向的对象的引用计数减1
//如果减1后为0,先释放掉p原来指向的内存,
//然后让q指向的对象的引用计数加1后赋值给p
//这个赋值操作符在减少左操作数的使用计数之前使rhs的使用计数加1
//从而防止自身赋值”而导致的提早释放内存
++(*rhs.use_count);
if ((--(*use_count)) == 0)
{
delete ptr;
ptr = nullptr;
delete use_count;
use_count = nullptr;
cout << "Left side object is delete!\n";
}
ptr = rhs.ptr;
use_count = rhs.use_count;
cout << "Assignment operator overloaded is called!\n";
return *this;
}
template <class T>
SmartPtr<T>::~SmartPtr()
{
//SmartPtr的对象会在其生命周期结束的时候调用析构函数
//在析构函数中检测当前对昂的引用计数,为0就释放掉
//不为0就说明还有其他的SmartPtr引用当前对象
//等待其他对象声明周期结束时调用析构函数进行释放
if (--(*use_count) == 0)
{
delete ptr;
ptr = nullptr;
delete use_count;
use_count = nullptr;
cout << "Destructor is called!\n";
}
}
template <class T>
T SmartPtr<T>::operator*()
{
return *ptr;
}
template <class T>
T* SmartPtr<T>::operator->()
{
return ptr;
}
template <class T>
T* SmartPtr<T>::operator+(int i)
{
T* tmp = ptr + i;
return tmp;
}
int main()
{
SmartPtr<int> p1(new int(0));
p1 = p1;
cout << *p1 << endl;
cout << p1.getcount() << endl;
SmartPtr<int> p2 = p1;
SmartPtr<int> p3(p1);
SmartPtr<int> p4(new int(1));
p4 = p1;
return 0;
}
(4)weak_ptr
weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。 weak_ptr 是用来解决 shared_ptr 相互引用时的死锁问题, 如果说两个shared_ptr 相互引用,那么这两个指针的引用计数永远不可能下降为 0,资源永远不会释放。 它是对对象的一种弱引用, 不会增加对象的引用计数, 和 shared_ptr 之间可以相互转化, shared_ptr 可以直接赋值给它, 它可以通过调用 lock 函数来获得 shared_ptr。
不能通过 weak_ptr 直接访问对象的方法, 比如 B 对象中有一个方法 print(), 我们不能这样访问, pa->pb_->print(); pb_是一个 weak_ptr, 应该先把它转化为shared_ptr, 如
shared_ptr<A> pa(new A());
shared_ptr<B> p = pa->pb_.lock();
p->print();