编程语言
首页 > 编程语言> > C++常见面试题总结

C++常见面试题总结

作者:互联网

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指针来区分;在类的非静态成员函数中返回对象本身,可使用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;			//正确

用指针有什么好处?

指针使用过程中有哪些注意事项?

指针和引用的区别?

引用常量不存在,没有int& const p。而常量引用是存在的cosnt int &p。

什么时候需要使用常引用?
如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用。引用型参数应该在能被定义为const的情况下,尽量定义为const 。常引用声明方式:const 类型标识符 &引用名=目标变量名;

引用的底层也是指针实现的,内置类型指针传递和引用传递的汇编代码是一样的,那C++为什么还需要引入引用呢?

用户自定义的类型最好用引用传参,这样可以避免不必要的构造函数和析构函数调用,但是对于内置类型,按值传参会比按引用传参更高效。

既然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();

标签:总结,面试题,函数,int,C++,引用,SmartPtr,ptr,指针
来源: https://blog.csdn.net/yuandenixin/article/details/120515062