概述
1、地址传递和引用传递区别?
C++中引用和指针的概念和区别?
先通过swap例子引出引用和指针,然后在Windows上通过VS2019,打断点,跳转到汇编指令。或者在linux上gcc -g,通过gdb调试转到汇编指令。转到用指针实现的swap和用引用实现的swap函数的汇编指令上看,引用和指针在底层的汇编指令是一样的。
拿32位系统来说,定义一个指针,底层开辟4个字节,然后把它指向的变量的内存地址放到这4个字节里面,当我们去查看汇编指令的时候,定义一个引用的时候也是在栈上开辟4字节的内存,把它所引用的变量的内存地址放到这4个字节里面,当我通过指针解引用的时候,实际上是从指针的这4个字节里取出它所指向的内存的地址,然后去访问它指向的内存。通过引用变量来引用它所指向的内存的时候,实际上汇编上也是从底层开辟好的4个字节的内存里取出地址,然后自动做一个解引用操作。两者在汇编指令上是一样的。
实际上,引用使用起来和指针一样,但是引用是必须要初始化的,使用的时候引用相对于指针来说更加安全。
但是指针也是很好的,可以通过指针在内存上漫游,可以通过指针的++,–在内存上偏移。
但是引用不能偏移,只要使用引用变量,就自动解引用了,永远访问的是它所引用的内存的值。
指针加1,加2都是在偏移,还可以求两个指针之间的距离:p2-p1
2、实际应用中传递一个很大的数组,不想去改变它,但是还不想让他拷贝,怎么做?
函数调用中涉及到传递,但是传递数组是根本不会发生拷贝的,传递数组名永远传递的是数组的起始地址!!!
数组在函数传递的过程中传递的是数组的起始地址,即数组的0号位置元素的起始地址,传的是一个指针(地址),不会针对整个数组的所有元素进行拷贝,所以,在传递数组的时候,既然只传递一个数组的起始地址而已,我们还要传入这个数组的长度进去,不然光拿到数组的起始地址,不知道数组的元素的个数,就不知道要操作多少个数据,除非传递是一个字符串数组,字符串数组可以根据/0判断末尾。
如果传递的不是一个字符串数组,就得再传递数组的长度。
传递的是一个数组的起始地址,在被调用函数中还是可以通过地址解引用修改数组的内容,如果想要控制只使用数组,而不能去改变数组的话,我们可以把形参的接收数组起始地址的指针变成一个const类型的指针,这样一来,被调用函数中就不能通过指针解引用修改数组的内容了。
const修饰*p了,*p不能访问了,但是p是可以改的,可以通过p指针的偏移访问数组的每一个元素位置,可以读,不能修改。
不只是数组,我们在传递容器的时候,vector是C++STL中重要的一个组件,我们实际使用最好使用vector,底层就是数组,但是vector不用自己管理数组的内存,可动态自动扩容。
当我们去传递一个容器的时候,因为容器和数组不一样,容器是对象啊,如果实参到形参是一个拷贝构造的过程,如果函数中传递的是一个容器,性能就降大了,要通过拷贝构造函数,根据实参的vector容器底层的内存的尺寸要给形参拷贝内存。此时我们应该:按引用传递!通过引用也可以修改容器,我们把形参变成const 常引用,只能读,不能改!
还可以说const在C/C++中的表现
3、const A * p 和 A * const p的区别?
看const修饰的是指针本身还是指针指向!
第1个式子:const修饰的是* p,* p不能改,但是p可以改。
第2个式子:const修饰的是p,p不能改,但是 * p可以改
从实践上看,第1个式子是应用在调用一个函数的时候,实参是通过指针传递实参的地址,形参是通过指针来接收的,在形参里面,我们只想访问实参,不想修改实参,所以我们形参采用的是const A * p ,这样一来我们可以读p,但是不能改p,有效的保护了在函数调用过程中只使用实参的值而不会去修改实参的值,这是非常重要的。
第2个式子表示,* p是可以修改的,但是p不能改,能通过p修改它指向的东西,但是不能修改p本身让p指向别的变量,这就是C++类成员方法的this指针的应用场景,我们不能给this赋值!!!this指针永远指向调用方法的对象的!!!this可以通过this指向修改它所指向对象的值,成员变量的值。
const a=10;//const修饰的常量不能被修改
如何改a的值?
方法1:
通过*p把a的改了。
const修饰的常量不能被修改,只是在编译阶段,语法级别上能识别出来,实际上,在真真正正的汇编上没有说是const修饰的内存,然后把内存属性改了,从原来的可读可写改成只能读不能写?不可能的。const只立足于语法层面,编译阶段编译器可以检测出来你对一个const关键字修饰的常量赋值了,但是经过编译以后,产生的汇编指令上,实际上我们在操作栈或者堆的时候,包括数据段,都是可读可写的,因为那时候没有const了,可以针对const修饰的量修改。
可以在C/C++代码中切入汇编代码:
函数栈上的变量都是通过ebp栈底指针偏移,
const并没有修改a内存的属性。
在C中,const是常变量
C++中,const是常量
4、成员函数后加const是什么意思?会有什么样的限制?
当我去定义一个常对象的时候,用常对象去调用一个普通方法的时候,发现是调用不了的,为什么调用不了?因为C++用一个对象调用一个方法,实际上汇编以后,调用的过程还是C函数的调用,只是把调用的这个方法的地址当做实参传递进去,所以,在成员方法编译的时候都会加一个形参:object* this,但是this是一个普通的指针,而常对象是object * const this指针,一个普通指针怎么可以接受一个实参的const常指针?类型转换失败!
所以当常对象调用成员方法的时候,这个成员方法要加const,加到函数的后面,让这个方法的普通的this指针变成constthis,这样形参的const * this就可以接收实参的constthis了。这样常对象就可以调用常成员方法了,在这个常方法里面,只能访问对象的成员变量而不能修改,因为const修饰的是*this了,不能通过this指针修改它指向的东西了。
如果给成员变量修饰一个mutable,那么在常方法里面依然是可以修改成员变量的
5、拷贝构造初始化(构造函数的大括号的初始化)和列表初始化(构造函数后面跟着:)的区别?类中const成员如何初始化?
初始化列表相当于指定初始化如果有成员对象的话,初始化列表指定对象构造的方式
构造函数的初始化:就是构造函数的大{},说明成员变量或者成员对象都构造过了,在{}之间只是做了赋值操作
所以,像定义变量必须初始化的那些东西,必须放在初始化列表(构造函数后的冒号:)进行初始化,比如说const常成员变量,在C++中,const和引用成员变量必须初始化,必须初始化的都要放到初始化列表上。
如果想指定vector初始化的时候,包含10个元素,初始化就要放在初始化列表:vec(10)
6、vector如何管理内存?
当我们想用一个顺序数组的时候,我们就可以用vector,vector相当于把数组封装起来了,而且提供了带[]的运算符重载函数,可以像使用数组一样去访问容器的下标,相当于数组来说,有一个好处:在我们实际编写程序的时候,如果是使用的是数组的话,它的内存是否足够,这是一个问题,内存定义太小了,不够用,定义太大了,浪费空间,有的时候我们可能用指针动态开辟内存数组,但是当内存满了,我又得自己去写内存扩容的代码。
vector底层是动态开辟内存的数组。
在windows下是1.5倍扩容,在linux下是2倍扩容。
vector有2个方法,一个是返回底层数据结构的容量:capacity
还有一个是size方法,返回底层数据结构的真真正正有效元素的个数。
我们构造一个vector,然后给vector循环去添加值,每一次循环的时候打印它的size和capacity,就能知道往vector里面放内容,它的扩容是怎样进行的。
所有C++的STL容器在去管理底层对象内存的时候,不可能用new和delete的,
vector只负责对象的构造、析构(construct,destroy),内存管理都是靠allocator空间配置器来管理的(内存的开辟和释放:allocate,deallocate)
在vector管理内存的时候一定要把new操作的内存开辟和对象构造分开,把内存释放和对象析构分开,否则在初始化vector的时候,如果用new,不仅仅会开辟内存,还会构造一大堆无用的对象,不符合我们的逻辑。
allocator在C++库中提供一个默认的allocator,它的allocate和deallocate的管理方式用的是默认的C库的malloc和free
如果我们使用vector比较频繁的话,而且是小块内存,我们可以采用内存池来替换allocator原本的malloc和free。然后讲内存池。
vector作为C++的容器对象,功能非常强大,有很多方法…
C++11提供的带右值引用的拷贝构造函数和赋值重载函数,很大程度上提高了vector的拷贝构造和赋值的性能。
7、vector什么时候会发生迭代器失效?(从编程习惯考虑如何避免)
当我们用vector去解决问题的时候,有可能涉及到去遍历1个vector,如果只是读取它的话,通过for或者foreach,迭代器就不会发生失效。
但是,当我们通过一个循环去不断的通过迭代器给vector进行添加或者删除操作,就有可能出现迭代器失效。
vector中的insert方法和erase方法,接收的都是迭代器,实际上,相关的容器的成员方法接收的都是迭代器
迭代器本质上是对指针的封装,迭代器底层的指针指向的是容器底层的数据结构,就是vector底层的数组,迭代器通过运算符的重载函数,以相同的方式可以遍历不同的容器,因为不同容器的遍历方式都被封装在迭代器的++,–运算符函数里面了。
如果在for循环中,多次给vector进行insert,甚至1个insert就导致vector扩容的位置都变了,迭代器就失效了,迭代器底层是指针,指针指向的地址(老内存)就失效了。
容器从底层实现的时候就有去判断,迭代器迭代的过程中,容器的元素有没有变化,如果变化了,不是增加就是删除,当前这个迭代器就要失效,怎么处理?insert或者erase处理完,返回的是新位置的迭代器,我们需要把这个迭代器循环变量更新一下。
insert时都失效;
erase时,删除后,后面的所有元素都要向前挪动,此时从删除位置到末尾元素的迭代器都失效了,因为元素的位置都挪动了;
怎么办???调用相关update方法时,及时更新迭代器
插入1次或者删除1次,就不用考虑,当前的迭代器失效了就失效了,反正不会再去用它,如果多次插入或者多次删除,一定用它的返回值把迭代器的循环变量更新一下。
迭代器对容器是非常重要的,迭代器不是所有迭代器所共享的,每种容器都有自己的迭代器,因为每个容器的底层数据结构是不一样的,但是实际上,用迭代器去写代码都是一样的。因为都封装在迭代器的++,–运算符的重载函数。用迭代器操作不同的容器的代码是一模一样的。
泛型算法应用于所有容器,泛型算法的参数也是迭代器。
8、用拷贝构造给一个复杂vector赋值不太好,如何解决?
意思就是vector里面装的是大对象,vector的拷贝构造会造成很多大对象的拷贝构造;(拷贝构造,赋值)
视情况采用std::move调用vector的右值引用拷贝构造,直接把右边底层的资源拿过来。
9、map/multimap区别
两个都是映射表,处理有关联关系的键值对。比如说用学生的学号对应学生的数据,通过快速查找学生的学号,来得到学生的数据。
map:key不能重复。底层是红黑树,增删查的时间复杂度是O(logn),效率非常快的
multimap:key可以重复。底层是红黑树,增删查的时间复杂度是O(logn),效率非常快的。
它们是应用在对key有排序要求的场景下。
但是我们大部分场景对key的有序性是没有要求的,所以我们一般使用的,或者是一些开源的库,C++标准库的源代码,是unordered_map或者unordered_set,使用的比较多,底层是哈希表,增删查的时间复杂度是O(1)
红黑树的应用场景很多:linux的虚拟内存vm管理,用的是红黑树,效率很高(早期是使用的是AVL树)。
AVL:基于内存的平衡树
B/B+树:基于I/O操作的平衡树,数据库的索引使用的。
10、智能指针
一般为了自动释放资源,某些资源用完了可以自动释放,防止资源泄漏。
智能指针:利用栈上的对象出作用域自动析构的特点,释放的代码写到智能指针的析构函数里面
在C语言中,使用堆内存,malloc和free
在C++中,使用堆内存,new和delete
但是这样去手动开辟和释放内存,有可能把free或者delete忘写了,或者文件资源忘记关闭了,或者在中间执行代码的时候提前return 走了。
带引用计数和不带引用计数的智能指针介绍,见我的博客《C++》
shared_ptr底层通过CAS操作,保证原子性,可以使用在多线程环境中。
强弱智能指针防止交叉引用问题。
enable_shared_from_this和shared_from_this获取当前对象的一个智能指针;
shared_from_this在当前对象的成员方法如何获取当前的shared_ptr智能指针。
不能通过当前成员方法的this指针直接创建一个shared_ptr!!!如果这样快一点的话,两个shared_ptr都认为只有自己持有了资源,释放资源的时候释放了多次。
C++11提供了make_shared:补充shared_ptr的坑,因为shared_ptr存在1个问题:它的操作分成2步:第一步,在构造一个shared_ptr的时候会给它传入一个new的一块内存,实际上shared_ptr底层还要去new一个控制资源的控制块,相当于内存块,记录资源的地址,资源的引用计数。现在假设:当我们去构造一个shared_ptr的时候,我们让它管理外部的资源,new成功了,但是它底层的控制块new失败的话,怎么办?相当于shared_ptr本身没有创建起来,最后那个资源就不会帮我们自动释放了!!!相当于智能指针本身的创建就有问题了。C++11以后给我们提供了make_shared,获取一个指向资源的shared_ptr,它是非常安全的。
C++14以后提供了一个make_unique,补的是unique_ptr的坑。
11、设计:10万个正整数,取值范围0到100w,互不重复,给定一个数字判断是否在这些数中出现过?
这就是在查重啊
哈希表可以查重:这10万个正整数构建1个哈希表,然后拿给定的数字在哈希表查,哈希表的增删查的时间复杂度是O(1)
哈希表解决这个问题的缺陷:哈希表都是空间换时间,占内存是非常大的,链式哈希表,发生冲突的元素是放在一个桶中,用一个节点表示1个元素,一个节点包含的是这个元素,和下一个元素的地址,如果拿4字节的正整数来说,如果有10万个正整数,意味着构建一张哈希表光存储元素就要10万乘以4=40万个字节,这只是放元素的,每一个元素的节点还包含下一个元素的地址,地址也是4字节,如果哈希表存放这10万个正整数,最终需要的内存字节是80万个字节,内存占用太大了啊,典型的空间换时间啊。
有个数,还有数字范围,省内存就使用位图法(用1个位表示一个数字是否存在,1个字节是8个位,1个字节就可以记录8个数字)详细的见我的其他博客
还有布隆过滤器(结合了哈希表和位图法)
如果是找出现过几次的话,位图法就用不了了
12、两个有序数组取交集?代码层面优化?n个数组取交集?
方法1:先用一个数组的元素构建一个哈希表,然后在用另一个数组的元素在这个哈希表里去查,可以取交集了。但是没有用到题目中的:有序数组的”有序“,不是最优方案,哈希表(O(m) * O(1)),哈希表占空间!
方法2:二分查找,遍历第1个数组的每个元素,拿第一个数组的每个元素在第二个数组里进行二分搜索,(O(m) * O(lgn) =O(mlgn)),不占额外的内存空间,而且效率高,但是还是没有把两个有序数组的“有序”都用上
方法3:二路归并的思想,双指针的思想,两个指针分别指向两个数组的首位置。如果这2个指针指向的元素是一样的,说明是交集的元素,一一比较,谁小谁往后挪动,(O(m)+O(n) = O(m+n));
如果两个数组的元素个数差不多的话,比如说m=n=1万,二路归并的时间复杂度就是O(2万)
二分查找的时间复杂度是O(1万·log1万)=O(1万·13)=O(12万)
此时,选择二路归并算法效率是高的。
如果两个数组的元素的个数差别特别大,选择二分查找算法。
n个数组取交集:把n个数组划分成两两数组取交集,然后再分为两两数组取交集。
或者n路归并。n个指针,每个指针指向一个数组的首位置,谁小谁往后挪动。
13、如何保证发送数据不丢且不重
协议,三方库,自己的应用。
muduo库已经实现高并发了。阐述muduo库。
TCP协议,协议保证了。
现在带宽资源丰富了,不用考虑TCP占带宽资源。
协议和应用;超时重传;序号和确认应答机制;滑动窗口;拥塞控制;tcp和udp协议,http和https协议,arp和rarp协议
14、线程池的线程为什么要动态扩容
进程池/线程池/内存池/连接池
muduo库的线程池不动态扩容,数量和CPU的核数相同,(设置4个线程,最大的利用CPU进行并行的处理)
muduo只是网络库。
为什么要做成动态扩容的?
当有一个发送文件的请求到达服务器的时候,服务器在epoll的一个工作线程里真的去给这个同学转发文件吗?会造成其他线程通信的操作没办法做。
这个文件很大,耗时,耗时的I/O操作需要有其他线程去做的。
数据库连接池就是动态扩容的
线程栈:8M
380+的线程(一起运行)
15、假设与数据库交互出现问题,线程在和数据库交互时产生阻塞,线程挂掉,会产生什么问题?
给出一种场景解答,逐渐明确问题 落实到怎么找代码上造成线程阻塞的地方 gdb调试
从连接池取连接
16、vector底层数据结构,为什么是二倍扩容
size(获取元素的个数) compacity(容量)
做循环,不断增加元素,然后打印这两个东西,看是怎么扩容。
涉及到扩容的均摊时间–倍数扩容,效率高
0 1 2 4 8 16 32 64 2倍扩容
其实1.5倍扩容最好
0 1 2 3 4 6 9 1.5倍扩容 利于内存的复用 内存碎片的复用
malloc - ptmalloc基于bin和chunk的内存,真真正正的内存都是在内核上管理的。
malloc第一次申请内存的时候,向内核申请的,内核都是以页面为单位的,一页给你,在ptmalloc管理起来,你malloc要4字节,就给你4字节,其他的分成块。
malloc按照页面 - 内核分配
在内核上分配内存是通过brk(分配小块内存,通过指针偏移分配,是连续的内存) mmap(分配大块内存)
扩容是重新分配内存,把旧内存释放掉。
当我们以二倍扩容的话,当我们要扩容新申请内存的时候,之前的已经被释放的内存是永远不会被复用起来了,新申请的内存的大小比之前释放的所有的内存的总和还要大。而1.5倍扩容,可以把之前的释放的内存复用。
顺序容器扩容的方式:1.5倍扩容是最多应用的。
17、map->红黑树和哈希表 关联容器 set和map unordered_set和unordered_map
map:排序的,应用于通讯录。
18、gcc是什么工具,编译链接原理
gnu(开源计划,包含很多有用的根据,包括linux)
gcc (编译器,编译的工具链,现在可以编译C/C++/JAVA等很多语言)
g++ (编译C++的)
gdb (调试)—coredump(核心的堆转储文件,作用:记录的是程序挂掉的时候内存的一个详细信息,以及各个线程的函数调用堆栈信息,可以帮助我们去定位程序为什么coredump挂掉了,我们用gdb直接调试coredump
我们用gcc编译cpp。gcc *.cpp
如果cpp代码中使用到C++IO流或者STL
则gcc 会产生链接错误!
因为gcc默认链接的是 libc.so
而不是libc++.so
g++默认链接的是libc++.so
19、linux下的进程状态,如何查看进程的状态
R :就绪和运行两个状态,不管是正在享受CPU的时间片,正在被调度,还是可以被调度,调度中,都属于R状态
S:sleeping睡眠状态,可中断的睡眠状态
D:不可中断状态,只能通过I/O事件唤醒
T :调试程序,程序进入stop状态
Z:僵尸态,进程结束了,内核PCB还没有被回收wait,占用内核资源,导致当前系统所能创建进程线程的数量变少
如何查看?
ps aux |less
Ss:当前进程包含子进程
S<:高优先级
SN:低优先级
Ssl:l表示是多线程程序
我们还可以
查看服务
本地进程间通信:信号量,管道,共享内存,消息队列
分布式:分布在不同机器的进程,同一台机器的进程的通信:RPC
线程通信:共享地址空间,通过共享数据段,堆
20、HTTP数据包组成,如何解析HTTP数据包
应用层协议
http
和 https(http + ssl 加密认证过程 签名 CA认证 对称加密和非对称加密)
我们打开浏览器:按F12打开调试工具
点到network上,网络,显示请求网络的时候所有发包的详细信息。
浏览器是BS结构,http/https
请求的页面是向后台请求一个html页面。
21、传输层采用TCP协议,在应用层读取数据时,如何判断是否读完,如果没读完,如何计算还剩下多少
TCP的粘包问题(流式服务)
在缓存满了,去发送。
如果是一发一应答,就没问题
如果是连续发多次,对方一次就收了:解决方法:给数据加数据头
一是从解决TCP粘包问题角度回答 二是从类似传输文件业务角度回答
秒传 filesize,md5值发送到服务端,服务端记录了已存储的所有的文件的md5值,如果匹配,则秒传OK
最后
以上就是秀丽耳机为你收集整理的628-C++复习总结的全部内容,希望文章能够帮你解决628-C++复习总结所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复