杂项 基础知识整体
1.c++中的类型转换: const_cast,static_cast,dynamic_cast,reinterpret_cast
c++中的智能指针:auto_ptr、shared_ptr、weak_ptr、 unique_ptr
参考如下:
1.类型转换
简单来说
const_cast-去除const
static_cast除了用在基础数据类型中 还可以对子类和父类之间指针和引用进行转换,编译时类型检查 向下转换的时候 如果出现问题直接崩溃
dynamic_cast:用在对子类和父类之间指针和引用进行转换 运行时检查 向下转换的时候 如果出现问题会返回null
reinterpret_cast<>():指针和引用对整形的转换,或者整形对指针和引用的转换 最不安全
2.a.智能指针
b.智能指针
auto_ptr:复制或者赋值都会改变资源的所有权 如果还调用之前的函数方法会崩溃
unique_ptr:禁止了复制或者赋值 本质就是重载了方法 如下
//禁止拷贝构造
unique_ptr(const unique_ptr<T>&sp) = delete;
//禁止赋值构造
unique_ptr<T>& operator=(const unique_ptr<T>&sp) = delete;
shared_ptr:可以记录引用特定内存对象的智能指针数量,当复制或拷贝时,引用计数加1,当智能指针析构时,引用计数减1,如果计数为零,代表已经没有指针指向这块内存,那么我们就释放它!这就是 shared_ptr 采用的策略!
weak_ptr:避免循环引用
2、重写与重载
重载:两个函数名相同,但是参数列表不同(个数,类型),返回值类型没有要求,在同一作用域中
重写:子类继承了父类,父类中的函数是虚函数,在子类中重新定义了这个虚函数,这种情况是重写
3.、一个C++源文件从文本到可执行文件经历的过程?
预处理-编译-汇编-链接
具体过程:参考一
预处理阶段:对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,成预编译文件。
编译阶段:将经过预处理后的预编译文件转换成特定汇编代码,生成汇编文件
汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件
链接阶段:将多个目标文件及所需要的库连接成最终的可执行目标文件
4.、C++的内存管理是怎样的?
在C++中,虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区以及栈区六部分。
代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
数据段:存储程序中已初始化的全局变量和静态变量
bss 段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量。
堆区:调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。
映射区:存储动态链接库以及调用mmap函数进行的文件映射
栈:使用栈空间存储函数的返回地址、参数、局部变量、返回值
5.虚函数的原理?虚表存放的位置?构造函数能不能是虚函数?
虚函数原理。含有虚函数的类有个虚表指针,虚指针指向虚函数表。虚函数表中存放着虚函数地址。派生类继承了父类的虚函数表,如果派生类虚函数会将自己的虚函数地址替换到虚函数表中。
C++中虚函数表位于只读数据段(.rodata),也就是C++内存模型中的常量区;而虚函数则位于代码段(.text),也就是++内存模型中的代码区。
构造函数不能是虚函数
参考一
4.union了解吗?为什么要内存对齐?内存对齐怎么实现的?
union即为联合,它是一种特殊的类。通过关键字union进行定义,一个union可以有多个数据成员。但是类所占的空间为类中最大的数据类型所占的大小,每个成员共享内存大小。
字节对齐
对齐的作用和原因:
平台问题:各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。并不支持从任何地址取数据
效率问题:如果不内存对齐,可能会两次内存访问。比如说
如果0x02~0x05存了一个int,读取这个int就需要先读0x01~0x04,留下0x02~0x04的内容,再读0x05~0x08,留下0x05的内容,两部分拼接起来才能得到那个int的值,这样读一个int就要两次内存访问,效率就低了。
6.C++强枚举类型用过吗?
C++11 引入了强类型枚举,也称为枚举类(enum class),它提供了更好的类型安全性。与传统的枚举类型相比,枚举类的成员是强类型的,不能隐式转换为其他类型,这有助于避免意外的类型转换错误。参考代码如下:
enum class Color {
Red, Green, Blue
};
// Color color = 1; // 错误:不能隐式转换为Color类型
Color color = Color::Red; // 正确:使用枚举类成员2.2 枚举类成员的底层类型
7.volatile关键字?
volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象
8.inline关键字?虚函数可以是inline的吗?inline的缺点?
内联函数和普通函数相比可以加快程序运行的速度,因为不需要中断调用,在编译的时候内联函数可以直接被镶嵌到目标代码中。inline是编译器的建议,不一定会展开,有可能也会想像一个普通的函数一样。因为inline函数减少了函数压栈出栈(时间),但是增加了内存开销(镶嵌到目标代码),但内存不够时,可能不会进行展开。
注意:内联函数和宏定义的区别
1.内联函数在运行时可调试,而宏定义不可以;
2.编译器会对内联函数的参数类型做安全检查或自动类型转换(同普通函数),而宏定
义则不会;
3.内联函数可以访问类的成员变量,宏定义则不能;
4.在类中声明同时定义的成员函数,自动转化为内联函数。
内联函数缺点以复制为代价,活动产函数开销
1)如果函数的代码较长,使用内联将消耗过多内存
2)如果函数体内有循环,那么执行函数代码时间比调用开销大。(因为 当调用函数的开销与函数体自身的开销相比无足轻重了,再使用内联函数就属多此一举。 )
虚函数可以是inline函数吗?
inline是在编译器将函数类容替换到函数调用处,是静态编译的。而虚函数是动态调用的,在编译器并不知道需要调用的是父 类还是子类的虚函数,所以不能够inline声明展开,所以编译器会忽略
C++类型转换说一下?dynamic_cast什么时候返回NULL?
10.C++智能指针了解吗?share_ptr怎么实现的?weak_ptr怎么实现的?
11.那你怎么确定weak_ptr的对象是否还存在呢?
12.赋值和初始化的区别?类构造函数初始化和初始化列表区别?
代码:
类的对象赋值和初始化
#include <iostream>
using namespace std;
class Point
{
public: Point(int a=0, int b=0):x(a), y(b){}; ~Point(){}; Point& operator =(const Point &rhs); int x; int y;
private: Point(const Point &p);
};
Point& Point::operator =(const Point &rhs)
{ x = rhs.x+1; y = rhs.y+1; return *this;
}
int main(void)
{ Point p(1,1); Point p1 = p; //初始化操作 Point p2; p2 = p; //赋值操作 cout<<"p1.x = "<<p1.x<<" "<<"p1.y="<<p1.y<<endl; cout<<"p2.x = "<<p2.x<<" "<<"p2.y="<<p2.y<<endl; return 0; }
构造函数与赋值函数的区别
1.复刻构造函数是一个对象来初始化一块内存区域,这块内存就是新对象的内存区
2.复刻构造函数式复制指针对象,赋值函数是引用指针对象
3.实现方式不一样,复刻构造函数首先是一个构造函数,它调用的时候好似通过参数传进来的那个对象来初始化一个对象,赋值函数就是一个已经初始化的对象来进行operator=操作
类构造函数初始化和初始化列表
Object::Object(itn _x,int _y):x(_x),y(_y)
{
}Object::Object(int _x,int _y)
{x = _x;y = _y;
}
初始化列表和构造函数初始化(赋值)的方式对于内置类型的成员(如int等)来说,其实没有什么区别,其性能和结果往往一样。
但是,对于非内置类型的成员(类类型)来说,是有区别的。
参考一
12.值传递和引用传递区别?
当函数形参为值传递时,在调用函数前,调用者会创建对象的副本当作实参,函数内部负责销毁该副本,而引用传递则是把指向对象的指针当作实参传递。
13.拷贝构造函数在什么时候用?深拷贝和浅拷贝区别?什么时候需要注意必须用深拷贝?
拷贝构造函数在什么调用?
简单来记的话就是,如果对象在声明的同时将另一个已存在的对象赋给它,就会调用复制构造函数;如果对象已经存在,然后将另一个已存在的对象赋给它,调用的就是赋值运算符(重载)
深拷贝和浅拷贝
C++ 拷贝构造函数分为浅拷贝和深拷贝两种,浅拷贝和深拷贝主要区别就是复制指针时是否重新创建内存空间。如果没有没有创建内存只赋值地址为浅拷贝,创建新内存把值全部拷贝一份就是深拷贝。浅拷贝在类里面有指针成员的情况下只会复制指针的地址,会导致两个成员指针指向同一块内存,这样在要是分别delete释放时就会出现问题,因此需要用深拷贝
什么时候使用深拷贝?
有指针的时候使用深拷贝,比如当前类中有有动态申请的内存空间。其他时候都用浅拷贝
14.C++内存模型讲一下?为什么堆栈生长方向一个是向上,一个是向下?
C++内存模型:
请你说一说C++的内存管理是怎样的?
参考回答:(来源于牛客)
在C++中,虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区以及栈区六部分。
代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
数据段:存储程序中已初始化的全局变量和静态变量
bss 段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量。
堆区:调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。
映射区:存储动态链接库以及调用mmap函数进行的文件映射
栈:使用栈空间存储函数的返回地址、参数、局部变量、返回值
堆栈生长方向一个是向上,一个是向下
堆栈 堆的生长方向向上,栈的生长方向下。 因为内存分配中,是堆,栈。堆向上栈向下后就可以最大的放数据,使得数据有效利用。
15.new和malloc的区别?怎么让new不报异常呢?讲下placement new?对void*是怎么看待的?
new和malloc的区别
1、new分配内存按照数据类型进行分配,malloc分配内存按照指定的大小分配;
2、new返回的是指定对象的指针,而malloc返回的是void*,因此malloc的返回值一般都需要进行类型转化。
3、new不仅分配一段内存,而且会调用构造函数,malloc不会。
4、new分配的内存要用delete销毁,malloc要用free来销毁;delete销毁的时候会调用对象的析构函数,而free则不会。
5、new是一个操作符可以重载,malloc是一个库函数。
6、malloc分配的内存不够的时候,可以用realloc扩容。扩容的原理?new没用这样操作。
7、new如果分配失败了会抛出bad_malloc的异常,而malloc失败了会返回NULL。
8、申请数组时: new[]一次分配所有内存,多次调用构造函数,搭配使用delete[],delete[]多次调用析构函数,销毁数组中的每个对象。而malloc则只能sizeof(int) * n。
怎么让new不报异常呢?
解决办法1 new 后面加 nothrow
char* p = (char*)-1;
while(p)p = new(std::nothrow) char[1000000];
cout<<"hello world"<<endl;
解决办法2
既然已经知道new可能抛出bad_alloc 异常,直接捕获就可以了
char *p =(char*)-1;while(p){try{p = new char[10000000];}catch(std::bad_alloc e){cout<<"new failed"<<endl;break;}}
placement new
一般来说,使用new申请空间时,是从系统的“堆”(heap)中分配空间。申请所得的空间的位置是根据当时的内存的实际使用情况决定的。但是,在某些特殊情况下,可能需要在已分配的特定内存创建对象(可能在栈也可能在堆),这就是所谓的“定位放置new”(placement new)操作。
定位放置new操作的语法形式不同于普通的new操作。例如,一般都用如下语句A* p=new A;申请空间,而定位放置new操作则使用如下语句A* p=new (ptr)A;申请空间,其中ptr就是程序员指定的内存首地址。考察如下程序。
#include <iostream>
using namespace std;class A
{
public:A(){cout << "A's constructor" << endl;}~A(){cout << "A's destructor" << endl;}void show(){cout << "num:" << num << endl;}
private:int num;
};int main()
{char mem[100];mem[0] = 'A';mem[1] = '\0';mem[2] = '\0';mem[3] = '\0';cout << (void*)mem << endl;A* p = new (mem)A;cout << p << endl;p->show();p->~A();getchar();
}
注意以下几点。
(1)用定位放置new操作,既可以在栈(stack)上生成对象,也可以在堆(heap)上生成对象。如本例就是在栈上生成一个对象。
(2)使用语句A* p=new (mem) A;定位生成对象时,指针p和数组名mem指向同一片存储区。所以,与其说定位放置new操作是申请空间,还不如说是利用已经请好的空间,真正的申请空间的工作是在此之前完成的。
(3)使用语句A *p=new (mem) A;定位生成对象时,会自动调用类A的构造函数,但是由于对象的空间不会自动释放(对象实际上是借用别人的空间),所以必须显示的调用类的析构函数,如本例中的p->~A()。
(4)如果有这样一个场景,我们需要大量的申请一块类似的内存空间,然后又释放掉,比如在在一个server中对于客户端的请求,每个客户端的每一次上行数据我们都需要为此申请一块内存,当我们处理完请求给客户端下行回复时释放掉该内存,表面上看者符合c++的内存管理要求,没有什么错误,但是仔细想想很不合理,为什么我们每个请求都要重新申请一块内存呢,要知道每一次内从的申请,系统都要在内存中找到一块合适大小的连续的内存空间,这个过程是很慢的(相对而言),极端情况下,如果当前系统中有大量的内存碎片,并且我们申请的空间很大,甚至有可能失败。为什么我们不能共用一块我们事先准备好的内存呢?可以的,我们可以使用placement new来构造对象,那么就会在我们指定的内存空间中构造对象。
下面是一个在堆上生成对象的例子。
对void是怎么看待的?
(1)void 是一个指针类型,指针变量都占4byte内存
(2)void 就像一张白纸,任何类型的指针都可以直接赋值给void类型的指针;但是反过来需要显示类型转换 例如下面例子
void* p=NULL;
int *a=NULL;
p=a;
double *b=NULL;
p=b;
char c[16]={0};
p=c;
int *a=NULL;
a=p; //err 会报错a=(int *)p;//需要强制类型转换
我觉得主要用途是 函数传参时不确定类型,或者要支持多类型的传参;
例如 My_memcpy的实现 (传参时不确定类型)
void* My_memcpy(void * dest, void * source, size_t count)
{void * ret = dest;while (count--){*(char *)dest = *(char *)source;dest = (char *)dest + 1;source = (char *)source + 1;}return ret;
}
16.C++编译过程了解吗?动态链接静态链接区别?
https://blog.csdn.net/qq_40765537/article/details/105940800
17.vector底层?map底层?unordered_map底层?
vector 底层是数组,map底层是红黑树 unordered_map底层是散列表
18.顺序存储和链式存储各自的优缺点?
循序储存如 vector
链式储存例如 list
请你说一说vector和list的区别,应用,越详细越好
参考回答:
1、概念:
1)Vector
连续存储的容器,动态数组,在堆上分配空间
底层实现:数组
两倍容量增长:
vector 增加(插入)新元素时,如果未超过当时的容量,则还有剩余空间,那么直接添加到最后(插入指定位置),然后调整迭代器。
如果没有剩余空间了,则会重新配置原有元素个数的两倍空间,然后将原空间元素通过复制的方式初始化新空间,再向新空间增加元素,最后析构并释放原空间,之前的迭代器会失效。
性能:
访问:O(1)
插入:在最后插入(空间够):很快
在最后插入(空间不够):需要内存申请和释放,以及对之前数据进行拷贝。
在中间插入(空间够):内存拷贝
在中间插入(空间不够):需要内存申请和释放,以及对之前数据进行拷贝。
删除:在最后删除:很快
在中间删除:内存拷贝
适用场景:经常随机访问,且不经常对非尾节点进行插入删除。
2、List
动态链表,在堆上分配空间,每插入一个元数都会分配空间,每删除一个元素都会释放空间。
底层:双向链表
性能:
访问:随机访问性能很差,只能快速访问头尾节点。
插入:很快,一般是常数开销
删除:很快,一般是常数开销
适用场景:经常插入删除大量数据
2、区别:
1)vector底层实现是数组;list是双向 链表。
2)vector支持随机访问,list不支持。
3)vector是顺序内存,list不是。
4)vector在中间节点进行插入删除会导致内存拷贝,list不会。
5)vector一次性分配好内存,不够时才进行2倍扩容;list每次插入新节点都会进行内存申请。
6)vector随机访问性能好,插入删除性能差;list随机访问性能差,插入删除性能好。
3、应用
vector拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随即访问,而不在乎插入和删除的效率,使用vector。
list拥有一段不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应使用list。
19.快排了解吗?时间复杂度?最坏情况是什么情况?我要得到一个升序序列,那么对于一个已经排好序的升序序列和已经排好序的降序序列,快排时间复杂度一样吗?快排快在哪里?
快排时间复杂度是 平均时间复杂度是nlong(n) 最好情况nlong(n),最坏是o(n2). 最坏的情况是元素已经有序 (从小到大或者从大到小) 快排快在其中有二分的思想。
下面给一个我喜欢用的快排代码
void quicksort(vector<int> &arr,int left,int right)
{int i, j, t, temp;if (left > right)return;temp = arr[left];i = left;j = right;while (i != j){while (arr[j] >= temp && i < j)j--;while (arr[i] <= temp && i < j)i++;if (i < j){swap(arr[i],arr[j]);}}//交换哨兵和确认所在位置i的数据swap(arr[left],arr[i]);cout << "已确认交换位置 i=" << i << endl;quicksort(arr,left,i-1);quicksort(arr, i +1,right);}
20.二叉树遍历方式?非递归的了解吗?AVL了解吗?红黑树了解吗?
二叉树遍历方式分为先序遍历,中序遍历,后序遍历
先序遍历:根左右
中序遍历:左根右
后序遍历:左右根
首先看看非递归写法
先序遍历
/*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}* };*/
class Solution {
private:vector<int> ret_v;public:vector<int> preorderTraversal(TreeNode* root) {stack<TreeNode* >s;if(root==nullptr){return ret_v;}s.push(root);while(!s.empty()){TreeNode* top=s.top();ret_v.push_back(top->val);s.pop();if(top->right){s.push(top->right);}if(top->left){s.push(top->left);}}return ret_v;}
};
后序遍历
我们可以将先序遍历的非递归写法改改即可,根左右==》根 右左=》左右根(reverse)
/*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}* };*/
class Solution {
private:vector<int> ret_v;
public:vector<int> postorderTraversal(TreeNode* root){if(root==nullptr){return ret_v;}stack<TreeNode*> s;s.push(root);while(!s.empty()){TreeNode * top=s.top();ret_v.push_back(top->val);s.pop();if(top->left){s.push(top->left);}if(top->right){s.push(top->right);}}reverse(ret_v.begin(),ret_v.end());return ret_v;}
};
中序遍历
左根右
/*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}* };*/
class Solution {private:vector<int > ret_v;
public: vector<int> inorderTraversal(TreeNode* root) {stack<TreeNode *> s;while(s.empty()!=true||root!=nullptr){//cout<<"进来了"<<endl;while(root!=nullptr){s.push(root);root=root->left;}TreeNode *top=s.top();cout<<top->val<<endl;ret_v.push_back(top->val);s.pop();root=top->right;}return ret_v;}
};
AVL:
AVL树是最先发明的自平衡二叉查找树。在AVL树中任何节点的两个子树的高度最大差别为1,所以它也被称为高度平衡树。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。
黑树是每个节点都带有颜色属性的二叉查找树,颜色或红色或黑色。 [3] 在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:
性质1. 节点是红色或黑色
性质2. 根节点是黑色
性质3.所有叶子都是黑色
性质4. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
性质5… 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
19.DFS?BFS?两者的实现?区别?
DFS是通过栈进行实现的 比如树的节点前序,中序,后序遍历
BFS是通过队列进行实现的 比如树结点的层序遍历
20.类的四个默认函数以及string 的自己实现
首先类的默认函数 参考一
#构造函数
#析构函数
#拷贝构造
#赋值重载
A(void); // 缺省的无参数构造函数A(const A &a); // 缺省的拷贝构造函数~A(void); // 缺省的析构函数A & operate =(const A &a); // 缺省的赋值函数
在实现的字符串中我们发现
a.一般的构造函数
String::String(const char* str)//通用构造
{if (!str){//为空。String a()length = 0;data = new char[1];*data = '\0';}else{length = strlen(str);data = new char[length + 1];strcpy(data, str);//会拷贝源的结束符}
}
b.拷贝构造函数(由于原指针是空,因此只涉及到赋值,没有释放问题)
使用形式是String str1=String (“zjs”); String str2=str1;
String::String(const String &str)//拷贝构造,深拷贝
{length = str.size();data = new char[length + 1];strcpy(data, str.c_str());
}
c.析构函数
String::~String()
{delete[] data;length = 0;
}
d.重载运算符“=”函数(里面涉及到将原指针释放重新赋值问题)
string str1=“meimei”;
string str2=“zjs”;
str2=str1;
String& String::operator=(const String &str)//赋值操作符4步
{if (this == &str) return *this;//1 自我赋值,返回自身引用delete[] data;//2 删除原有数据length = str.size();//3 深拷贝data = new char[length + 1];strcpy(data, str.c_str());return *this;//4 返回自身引用
}
我们可以发现其实string类有一个char * p指针,和一个记录长度的数字
#include <iostream>using namespace std;class String
{
public:String(const char* str = NULL);//通用构造函数,String("abc")String(const String &str);//拷贝构造~String();String& operator=(const String &str);//赋值运算符。返回引用String operator+(const String &str) const;String& operator+=(const String &str);//+=操作符。返回引用char& operator[](int n) const;//下标操作符。返回引用bool operator==(const String &str) const;int size() const;//字符串实际大小,不包括结束符const char *c_str() const;//将string转为char *private:char *data;int length;
};String::String(const char* str)//通用构造
{if (!str){//为空。String a()length = 0;data = new char[1];*data = '\0';}else{length = strlen(str);data = new char[length + 1];strcpy(data, str);//会拷贝源的结束符}
}String::String(const String &str)//拷贝构造,深拷贝
{length = str.size();data = new char[length + 1];strcpy(data, str.c_str());
}String::~String()
{delete[] data;length = 0;
}String& String::operator=(const String &str)//赋值操作符4步
{if (this == &str) return *this;//1 自我赋值,返回自身引用delete[] data;//2 删除原有数据length = str.size();//3 深拷贝data = new char[length + 1];strcpy(data, str.c_str());return *this;//4 返回自身引用
}
String String::operator+(const String &str) const//+操作符3步
{//新建对象包括新空间,拷贝两个数据,返回新空间String newString;newString.length = length + str.size();newString.data = new char[newString.length + 1];strcpy(newString.data, data);strcat(newString.data, str.data);return newString;
}String& String::operator+=(const String &str)//+=操作符5步
{//重分配新空间,拷贝两个数据,删除自己原空间,赋值为新空间,返回引用length += str.size();//成员length是实际长度char *newdata = new char[length + 1];strcpy(newdata, data);strcat(newdata, str.c_str());delete[] data;data = newdata;return *this;
}char& String::operator[](int n) const
{//下标操作符,返回引用if (n >= length) return data[length - 1];//如果越界,返回最后一个字符else return data[n];
}bool String::operator==(const String &str) const
{if (length != str.size()) return false;return strcmp(data, str.c_str()) ? false : true;
}int String::size() const
{return length;
}const char *String::c_str() const
{return data;
}int main()
{char a[] = "Hello", b[] = "World!";String s1(a), s2(b);cout << s1.c_str() << endl;cout << s2.c_str() << endl;s1 += s2;cout << s1.c_str() << endl;s1 = s2;cout << s1.c_str() << endl;cout << (s1 + s2).c_str() << endl;cout << s1.size() << endl;cout << s1[1] << endl;if (s1 == s2)cout << "相等" << endl;
}
疑问:
当我们重载运算符+号是,如果拷贝构造函数参数列表中不加入const
String::String(String &str)//拷贝构造,深拷贝
{length = str.size();data = new char[length + 1];strcpy(data, str.c_str());
}
我们的重载"+"运算符还是
String String::operator+(const String &str) const//+操作符3步
{//新建对象包括新空间,拷贝两个数据,返回新空间String newString;newString.length = length + str.size();newString.data = new char[newString.length + 1];strcpy(newString.data, data);strcat(newString.data, str.data);return newString;
}
此时在编译器中将会报错
我没想通,也就是+运算符返回值是默认的const类型?因此找不到构造函数?(const在参数列表中是可以实现函数重载的)
21.各种排序算法
参考一
22.计算机网络相关:
tcp 三次握手 四次释放 参考如下
参考一
活动窗口
23.字节对齐
参考一
24.说说什么是虚继承,解决什么问题,如何实现?
参考一
25.stl迭代器失效
参考一:
dataMap.erase(iter++);这句话分三步走,先把iter传值到erase里面,然后iter自增,然后执行erase,所以iter在失效前已经自增了。
数组型数据结构:该数据结构的元素是分配在连续的内存中,insert和erase操作,都会使得删除点和插入点之后的元素挪位置,所以,插入点和删除掉之后的迭代器全部失效,也就是说insert(*iter)(或erase(*iter)),然后在iter++,是没有意义的。解决方法:erase(*iter)的返回值是下一个有效迭代器的值。 iter =cont.erase(iter);
链表型数据结构:对于list型的数据结构,使用了不连续分配的内存,删除运算使指向删除位置的迭代器失效,但是不会失效其他迭代器.解决办法两种,erase(*iter)会返回下一个有效迭代器的值,或者erase(iter++).
树形数据结构: 使用红黑树来存储数据,插入不会使得任何迭代器失效;删除运算使指向删除位置的迭代器失效,但是不会失效其他迭代器.erase迭代器只是被删元素的迭代器失效,但是返回值为void,所以要采用erase(iter++)的方式删除迭代器。