【C++难点收录】“为什么C++难,你真的理解了这些吗?”《常见面试题》

举报
xcc-2022 发表于 2022/09/16 20:21:29 2022/09/16
【摘要】 @[toc] 前言众所周知,C++为什么会难,到底难在哪里?小编通过本文告诉你;坑已填好,欢迎路过。 类与对象临时变量具有常性(函数,引用)内联函数没有地址 隐含this指针class Data{void Init(int year=0,int month=3,int day=4) { _year=year; _month=month; _day=...

@[toc]

前言

众所周知,C++为什么会难,到底难在哪里?小编通过本文告诉你;坑已填好,欢迎路过。

类与对象

临时变量具有常性(函数,引用)

内联函数没有地址

隐含this指针

class Data
{
void Init(int year=0,int month=3,int day=4)
    {
      _year=year;
        _month=month;
        _day=day;
        
        //类似于
        this->_year=year;
        this->_month=month;
        this->_day=day;
    }
    
};

this指针存在栈上,因为它是一个形参

image-20220822080811313

#include<iostream>
using namespace std;

class A
{
public:
  
	void print()
	{
		cout << _a2 << endl;//this->_a2
	}
	void show()
	{
		cout << "show()"<<endl;
	}
private:
	int _a2;
};

int main()
{

	A *p = NULL;//p->print(p);
	p->print();//这一行会引发什么?编译不通过?程序崩溃?正常运行?//崩溃
	p->show();//这一行会引发什么?编译不通过?程序崩溃?正常运行?//正常运行
	//成员函数存在公共的代码段,所以p->show()这里不会取p指向的对象上找
	//访问成员函数,才会去找

	A a;
	a.print();//p->print(&a);

	return 0;
}

构造函数和析构函数

Date d1;//我们不写编译器生成 2,全缺省的 3.无参数的  -》 默认构造函数 -》不传参数可以调用的

我们不写,编译器生成构造函数和析构函数

内置类型/基本类型 (int/char)不会处理

自定义类型 调用它的构造函数初始化/析构函数(new /delete)

class Time
{
	~Time()
	{
	
	}
};
class Date
{
private:
	int _size;
	int _capaciti;
	
	Time _t;//自定义类型

};

拷贝构造

#include<iostream>
using namespace std;

class Date
{ 
public:
	Date(int year = 0, int month = 3, int day = 4)
	{
		_year = year;
		_month = month;
		_day = day;
	}
    //err
	/*Date(Date d2)
	{
		_year = d2._year;
		_month = d2._month;
		_day = d2._day;
	}*/
private:
	int _year;
	int _month;
	int _day;
};
int main()
{

	Date d1(2022,10,10);
	
    //这两个写法都调用拷贝构造
	Date d2(d1);//调用拷贝构造
	Date d3 = d2;
    
	return 0;
}

image-20220822102953807

有三种传参方式:

  1. 传值(d2)传参需要拷贝构造,然后拷贝的时候会发生递归,不会构造
  2. 传址(*d2)
  3. 传引用(&d2)会直接构造,因为引用是它的别名,直接进入了

建议:传入引用

	Date(Date& d2)//类似于 Date &d2=d1;
	{
		_year = d2._year;
		_month = d2._month;
		_day = d2._day;
	}

然后我们把赋值反过来:d2._day=__day;编译运行通过了

image-20220822104700034

这是因为我们把未初始的_day赋值给了d2,未初始化都是随机值,这里也是引用,改变了原有的值,所以需要加上const防止改变

Date(const Date& d2)

内存管理

int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
static int staticVar = 1;
int localVar = 1;
int num1[10] = {1, 2, 3, 4};
char char2[] = "abcd";
char* pChar3 = "abcd";
int* ptr1 = (int*)malloc(sizeof (int)*4);
int* ptr2 = (int*)calloc(4, sizeof(int));
int* ptr3 = (int*)realloc(ptr2, sizeof(int)*4);
free (ptr1);
free (ptr3);
}
1. 选择题:
选项: A.B.C.数据段 D.代码段
globalVar在哪里?____ staticGlobalVar在哪里?____
staticVar在哪里?____ localVar在哪里?____
num1 在哪里?____
char2在哪里?____ *char2在哪里?___
pChar3在哪里?____ *pChar3在哪里?____
ptr1在哪里?____ *ptr1在哪里?____

答案:CCCAAAAADAB

2. 填空题:
sizeof(num1) = ____;
sizeof(char2) = ____; strlen(char2) = ____;
sizeof(pChar3) = ____; strlen(pChar3) = ____;
sizeof(ptr1) = ____;


答案:40544/844/8

image-20220823173904630

image-20220823174715516

【说明】

  1. 栈又叫堆栈,非静态局部变量/函数参数/返回值等等,栈是向下增长的。

  2. 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共
    享内存,做进程间通信。(Linux课程如果没学到这块,现在只需要了解一下)

  3. 堆用于程序运行时动态内存分配,堆是可以上增长的。

  4. 数据段–存储全局数据和静态数据。

  5. 代码段–可执行的代码/只读常量。

操作系统内存管理:

  1. 分段:不同用途数据放到不同的区域,就像我们现实中也会对省市乡级划分区域为一样

  2. 分页:

填空题

image-20220823174239686

operator new和operator delete函数

我们相同的方法不同的操作来申请空间

A* p1=(A*)malloc(sizeof(A));
A* p2=new A;//会调用构造函数
A* p3 = (A*)operator new(sizeof(A));

operator new和malloc的区别?我们先来看看申请空间错误的情况是什么样,首先是malloc:

image-20220823211307051

代码:

size_t size = 2;//无符号类型
void *p4 = malloc(1024 * 1024 * 1024 * size);

image-20220823211703049

这里我们申请失败返回0,我们再看operator new:

代码:

void *p5 = operator new(1024 * 1024 * 1024 * size);
cout << p5 << endl;

image-20220823211930509

这里失败抛出异常(面对对象处理错误的方式)

结论:

使用方式都一样,处理错误的方式不太一样

operator new销毁方式:operator delete(p5);

最后:这里我们要注意,为什么在.h文件不用包含任何头文件库和命名空间,是因为我在.cpp中包含头文件的前面先加入了需要编译后展开的using 和头文件,如果命名空间放到文件后,string.h头文件就会找不到cout和cin,因为需要展开才能使用到(先后问题)

在这里插入图片描述

模板

T ADD(T&a,T&b)ADD(a1,a2)T的类型是编译器自己推导的(隐式),

ADD<int>(a1,a2)(显示)

我们用模板实现栈:

#include<iostream>
#include<assert.h>
using namespace std;
template<class T>
class stack
{
public:
	stack()
		:_a(nullptr)
		, _size(0)
		, _capcity(0)
	{}

	~stack()
	{
	
		delete[]_a;
		_a = nullptr;
		_size = 0;
		_capcity = 0;
	}
	//这里为了访问私有通过公共接口返回_size
	size_t size()
	{
		return _size;
	}
	
	//类里面声明,类外面定义stack<T>是一个类型
	T& operator[](size_t i);
	void push_back(const T&x);
	void pop_back();


private:
	T*_a;
	size_t _size;
	size_t _capcity;

};

template<class T>
T& stack<T>::operator[](size_t i)
{
	assert(i <_size);
	return _a[i];
}


template<class T>//声明模板类型T,因为已经与类分离了,函数类型是stack<T>
void stack<T>::push_back(const T&x)
{
	//空间不够,需要增容
	if (_size == _capcity)
	{
		int newcapticy = _capcity == 0 ? 2 : _capcity * 2;
		T*tmp = new T[newcapticy];
		//
		if (_a)
		{
			//把旧的数据拷贝到新的空间
			memcpy(tmp, _a, sizeof(T)*_size);
			delete[]_a;
			
		}
		_a = tmp;
		_capcity = newcapticy;
	}
	_a[_size] = x;
	_size++;
}

template<class T>
void stack<T>::pop_back()
{
	assert(_size > 0);
	_size--;
}


int main()
{

	stack<int>v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);
	v1.push_back(6);
	for (size_t i = 0; i < v1.size(); i++)
	{
		v1[i] *= 2;
	}


	for (size_t i = 0; i < v1.size(); i++)
	{
		//这里的v1[i]会调用opertor[]第i个数据
		cout << v1[i]<<" ";
	}
	cout << endl;

	return 0;
}

引用的奇淫飞技

当我们要修改函数返回的值,只能加上引用,否则会报错“不可修改的左值”,还可以减少拷贝

image-20220824154121996

string

模仿实现string

我们创建一个string.h头文件

class string
	{
	public:
		string():_str(nullptr){}
		string(char*str) :_str(str){}
		size_t size()
		{
			return strlen(_str);
		}
		char& operator[](size_t i)
		{
			return _str[i];
		}
	

	private:
		char*  _str;
};

既然模仿,那肯定得有自己的sting,我们namespace一个名字,不然库的string会搞混:

namespace copy
{
	class string
    {
        .....
    };
}

我们再写一个测试函数变量字符串:

void test_string1()
	{
		string s2();
		string s1("helllo");
		for (size_t i = 0; i < s1.size(); i++)
		{
		
			std::cout << s1[i] << " ";
		}
		std::cout << std::endl;
	}

然后在.cpp源文件中这样写:

#include<iostream>
#include"string.h"
using namespace std;


int main()
{
	copy::test_string1();
}

image-20220825150041110

遍历没有问题,那我们修改呢?崩溃了,原因是函数都存在栈中,字符串在代码段中,不能修改。

image-20220825150513024

image-20220825150218268

我们知道一个string的库函数接口不止有修改还有插入还有空间不够怎么办?我们尝试在堆上申请空间,让指针指向这个堆,并且把字符串拷贝到堆里。

image-20220825150756677

那应该在哪里开辟一段空间呢?在构造函数中,malloc和new可以完成,但这里是自定义类型,内置类型它们效果一样,所有我们用new,那申请多少个空间好呢?

string(char*str):_str(new char[strlen(str)+1]){}

计算字符串大小,并+1,因为在我们使用字符串的时候,后面都带有\0,而在在c_str接口的时候也是需要一个默认\0;

最后拷贝:strcpy(_str, str)

然后,再实现一个空字符串遍历,默认的std::string s2;遍历不会报错,而我们的现在会崩溃,原因是,我们用this指针指向str这个空字符默认只有\0我们对空解引用就出错了;这里不能直接给他nullptr,new一个给他;

string():_str(new char[1])
		{
			_str[0] = '\0';

		}

析构函数:

~string()
		{
			delete[]_str;
			_str = nullptr;
		}
string s1("helllo");
string s2(s1);

上面代码会崩溃,原因是s2拷贝了s1的字符串和空间,因为我们没有写拷贝构造函数,编译器默认生成,这会造成同一个空间被析构两次【浅拷贝】

深拷贝:(把~string(){}注释掉,不然一样报错)

//s2=s1;
		string& operator=(const string& s)
		{
			if (this != &s)
			{
			char*tmp = new char[strlen(s._str) + 1];
			strcpy( tmp,s._str);
			delete[]_str;
			_str = tmp;
			}
			
			return *this;
		}

底层原理:

using namespace std;

namespace test{

	class string
	{
	public:
		
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}

		//这里需要对它解引用,不能直接nullptr(默认为空)
		string(char* str="") :_str(new char[strlen(str)+1])
		{
			strcpy( _str,str);
		}
		//传统手法
		//s2(s1)
		/*string(const string &s)
			:_str(new char [strlen(_str)+1])
		{
			strcpy(_str, s._str);
		}
	
		//现代手法
		//s2(s1)
		string(string & s) :_str(nullptr)
		{
			string tmp(s._str);
			swap(_str, s._str);
			
		}
		//s2=s3;
		string&operator=(string s)
		{
			swap(_str, s._str);
			return *this;
		}
		//const s2=s3;
		string&operator=(const string& s)
		{
			if (this != &s)
			{
				string tmp(s._str);
			swap(_str,tmp._str);//等同swap(_str, s._str);
			return *this;
			
			}
		}
		string(){}
		
		
		size_t size()
		{
			return strlen(_str);
		}
		
		const char* c_str()
		{
			return _str;
		}

		char& operator[](const size_t& i)
		{
			return _str[i];
		}


		~string()
		{
			delete[]_str;
			_str = nullptr;
		}*/

	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};

	void test_string1()
	{

		string s1("hello world");
		string s3="world";
		
		
		for (size_t i = 0; i < s1.size(); i++)
		{
			s1[i] += 1;
			cout << s1[i]<< " ";
		}
		cout << endl;
		string s2 = s3;
		cout << s2.c_str() << endl;

		
	}
	void test_string2()
	{
		string s1("hello world");
		string s2(s1);
		cout << s2.c_str() << endl;
	}
	void test_string3()
	{
		string s1("hello world");
		string::iterator it = s1.begin();
		while (it != s1.end())
		{
			cout << *it << " ";
			++it;
		}
		
		cout << endl;
		for (auto e : s1)
		{
			cout << e << " ";
		}
		cout << endl;
	}


}

image-20220826071654086

面试题

  1. 为什么空类占1一个字节,不是0?

一个字节不是为了存取数据,而是占位

当我们实例化一个对象,A1 a;我们需要用它,如果字节0,那我们怎么找它调用呢?

class A1
{
void printf();
};
int main()
{
    A1 a;
    return 0;
}
  1. static
int Add()
{
	static int c=1;//只会初始化一次
	c=a+d;
	return c
}
  1. 全局变量和static变量有什么区别?

  2. malloc/calloc/realloc有什么取别

​ malloc:申请空间

​ calloc:申请空间加初始化为0;

​ realloc:对已有的空间进行扩容

  1. malloc/free和new/delete的区别
    malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是:

    1. malloc和free是函数,new和delete是操作符

    2. malloc申请的空间不会初始化,new可以初始化

    3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可

    4. malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型

    5. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常

    6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间
      后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理

    7. 出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会
      导致响应越来越慢,最终卡死

  2. 内存泄漏

    1. 1 什么是内存泄漏,内存泄漏的危害
      什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不
      是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而
      造成了内存的浪费。
      内存泄漏的危害:长期运行的程序
void MemoryLeaks()
{
// 1.内存申请了忘记释放
int* p1 = (int*)malloc(sizeof(int));
int* p2 = new int;
// 2.异常安全问题
int* p3 = new int[10];
Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
delete[] p3;
}
  1. 如何避免内存泄漏

    1. 1工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状
      态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保
      证.

      7.2 采用RAII思想或者智能指针来管理资源。

      7.3 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。

      7.4 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。

      总结一下:
      内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工
      具。

  2. 如何一次在堆上申请4G的内存?

// 将程序编译成x64的进程,运行下面的程序试试?
#include <iostream>
using namespace std;
int main()
{
void* p = new char[0xfffffffful];
cout << "new:" << p << endl;
return 0;
}
  1. 实现一个string
//这里需要对它解引用,不能直接nullptr(默认为空)
		string(char* str = "") :_str(new char[strlen(str) + 1]), _size(0)
		{
			strcpy(_str, str);
		}
		string()
		{
		}
		
		//深拷贝-》传统手法
		//s1=s2;
		string&operator=(string & s)
		{
			if (this != &s)
			{
				char * tmp = new char[strlen(s._str) + 1];
				strcpy(tmp, s._str);
				delete[]_str;
				_str = tmp;

			}
			return *this;
		}

		//s2(s1)
		string(const string &s)
			:_str(new char[strlen(_str) + 1])
		{
			strcpy(_str, s._str);
		}

		////深拷贝-》现代手法
		////s2(s1)
		//string(string & s) :_str(nullptr)
		//{
		//	string tmp(s._str);
		//	swap(_str, s._str);

		//}
		////s1=s2;
		//这里最巧的就是传值操作
		//string&operator=(string s)
		//{
		//	swap(_str, s._str);
		//	return *this;
		//}
		////const s2=s3;
		//string&operator=(const string& s)
		//{
		//	if (this != &s)
		//	{
		//		string tmp(s._str);
		//		swap(_str, tmp._str);//等同swap(_str, s._str);
		//		return *this;

		//	}
		//}
  1. vector插入数据如何实现的?

(增容次数更多,效率更低,因为每次增容都要付出代价)

2倍:相对而言效率更好,但是浪费的空间更多。插入1067个数据,(倒数最后一个1024*2)最终到2048,浪费970多个

1.5倍:1067个数据,总容量1599(倒数一个1066*1.5)

答:增容多少是一种旋转,各有利弊,均衡一点,可以使用reserve(1067);

  1. 为什么会有list?

vector的缺点是什么?

答:1.头部和中部的插入删除效率低,O(N),因为需要挪动数据;

​ 2.插入数据空间不勾 需要增容,增容需要开新空间、拷贝数据、释放旧空间,会付出很大的代价

优点:

1.支持下标的随机访问、间接的就很好的支持排二分查找、堆算法等等

list出现就是为了解决vector的缺陷

优点:

1.list头部、中间插入不再需要挪动数据,效率高O(1)

2.list插入数据是新增节点,不再需要增容

缺点:

1.不支持随机访问。

所有实际使用中vector和list是相互相成的两个容器

  1. 什么是迭代器失效
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。