C++一些知识回顾
多态
简单概括:一个接口,多种方法。
多态性:相同对象收到不同消息或不同对象收到相同消息时产生不同的动作。
种类:
编译时多态(静态多态),通过重载函数实现。
主要体现在函数模板。
#include <iostream> template <typename T> T add(T a,T b) { T c = a + b; return c; } int main() { int i1 = 1; int i2 = 2; int result = 0; result = add(i1,i2); std::cout << "result: "<<result<<std::endl; }
函数重载不是多态。
重载:一个类中,函数名相同,参数列表不同。编译器会根据参数列表,生成不同名称的预处理函数。没有体现多态。
重写:覆盖父类中相同名称相同参数的虚函数。
运行时多态(动态多态),通过虚函数实现。
程序运行的时候,动态绑定所需要的函数,动态找到函数的调用入口,从而确定具体调用哪个函数。
#include <iostream> using std::cout; using std::endl; class parent { public: parent() {} virtual void eat() { cout << "Parent eat." << endl; } void drink() { cout << "Parent drink." << endl; } }; class child: public parent { public: child() {} void eat() { cout << "Child eat." << endl; } void drink() { cout << "Child drink." << endl; } }; int main() { parent *pa = new child(); pa->eat(); // Child eat. pa->drink(); // Parent drink. return 0; }
两个函数调用有区别就是因为
drink
不是虚函数,所以父类指针要调用父类里面的函数。而eat
是虚函数,就会找实际子类对象的函数。父类的指针只能访问子类中重写了父类中的那些虚函数,而不能访问子类新增的特有的函数。
如果类里定义了
virtual void ear() = 0;
,那这个类就是抽象类,不能定义对象。
智能指针
帮忙管理动态分配的内存的,避免内存泄漏。
内存泄漏
分配了一块内存,但是不再持有引用了,但是没有收回,这块泄漏的内存再整个程序生命周期都不可再使用。
一般是分配了堆内存没有释放。更容易犯的错误就是在一个函数里分配了内存,别的函数调用,但是没有释放。
用valrind
定位内存泄漏的原因。
内存泄漏例子
#include <iostream>
using std::cout;
using std::endl;
class Test
{
public:
Test() { cout << "hi" << endl; }
~Test() { cout << "bye" << endl; }
private:
int debug = 20;
};
int main()
{
Test *test = new Test();
// hi
return 0;
}
valgrind检测
$ valgrind --tool=memcheck --leak-check=full ./a.out
==17915== Memcheck, a memory error detector
==17915== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==17915== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==17915== Command: ./a.out
==17915==
hi
==17915==
==17915== HEAP SUMMARY:
==17915== in use at exit: 4 bytes in 1 blocks
==17915== total heap usage: 1 allocs, 0 frees, 4 bytes allocated
这里不完全准确,分配和释放次数,因为C++分配内存时,为了提高效率,使用了内存池,程序终止时内存才会被回收。
==17915==
==17915== 4 bytes in 1 blocks are definitely lost in loss record 1 of 1
==17915== at 0x4C2A593: operator new(unsigned long) (vg_replace_malloc.c:344)
==17915== by 0x400971: main (test.cpp:17)
这里指出了泄漏所在的代码位置
==17915==
==17915== LEAK SUMMARY:
==17915== definitely lost: 4 bytes in 1 blocks
==17915== indirectly lost: 0 bytes in 0 blocks
==17915== possibly lost: 0 bytes in 0 blocks
==17915== still reachable: 0 bytes in 0 blocks
==17915== suppressed: 0 bytes in 0 blocks
==17915==
==17915== For lists of detected and suppressed errors, rerun with: -s
==17915== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
总结:有一个泄漏
auto_ptr
已被抛弃,用unique_ptr代替
#include <iostream>
#include <memory>
using std::cout;
using std::endl;
class Test
{
public:
Test() { cout << "hi" << endl; }
~Test() { cout << "bye" << endl; }
int getDebug() { return debug; }
private:
int debug = 20;
};
int main()
{
std::auto_ptr<Test> test(new Test());
cout << test->getDebug() << endl;
// hi
// 20
// bye
return 0;
}
抛弃原因
复制或者赋值会改变资源所有权
int main() { std::auto_ptr<Test> p1(new Test(1)); std::auto_ptr<Test> p2(new Test(2)); cout << p1->getDebug() << endl; cout << p2->getDebug() << endl; p1 = p2; cout << p1->getDebug() << endl; // 2 cout << p2->getDebug() << endl; // segmentfault return 0; }
p1=p2
语句:释放p1托管的指针,接收p2托管的指针,p2所托管的指针指向null。不支持数组对象的内存管理。
unique_ptr
基本是auto_ptr
的替代品。
特性:
- 两个指针不能指向同一个资源。
- 无法进行左值复制构造、复制赋值,但允许临时右值构造和赋值。
- 会自动释放。
- 容器中保存指针是安全的。
#include <iostream>
#include <memory>
#include <vector>
#include <string>
using namespace std;
class Test
{
public:
Test(int size):sz(size) {
data = new int[sz];
cout << "Test" << endl;
}
// copy constructor
Test(const Test &t):sz(t.sz) {
data = new int[sz];
for (int i = 0; i < sz; i++) {
data[i] = t.data[i];
}
cout << "Test copy" << endl;
}
// move constructor
Test(Test&& t):sz(t.sz) {
data = t.data;
sz = t.sz;
t.sz = 0;
t.data = nullptr;
cout << "Test move constructor" << endl;
}
~Test() {
delete [] data;
cout << "~Test" << endl;
}
int *data;
int sz;
};
int main()
{
std::unique_ptr<string> p1(new string("hi"));
std::unique_ptr<string> p2(new string("hi2"));
// 不允许
// p1 = p2;
// unique_ptr<string> p3(p2);
unique_ptr<string> p3(std::move(p1));
p1 = std::move(p2);
cout << "p1 = p2 赋值后:" << endl;
cout << "p1:" << p1.get() << endl;
cout << "p2:" << p2.get() << endl;
cout << "p3:" << p3.get() << endl;
// p1 = p2 赋值后:
// p1:0x23b3060
// p2:0
// p3:0x23b3010
return 0;
}
shared_ptr
指针共享,引用计数。
weak_ptr
配合shared_ptr引入的,不会引起计数的改变。不能访问数据。
如shared_ptr存在循环引用,就没法释放资源,引起内存泄漏。
如果weak_ptr指向的对象存在,lock函数返回一个shared_ptr,否则是空的shared_ptr.
解决循环引用问题
#include <iostream>
#include <memory>
#include <vector>
#include <string>
using namespace std;
class ClassB;
class ClassA
{
public:
ClassA() { cout << "ClassA Constructor..." << endl; }
~ClassA() { cout << "ClassA Destructor..." << endl; }
weak_ptr<ClassB> pb; // 在A中引用B
};
class ClassB
{
public:
ClassB() { cout << "ClassB Constructor..." << endl; }
~ClassB() { cout << "ClassB Destructor..." << endl; }
weak_ptr<ClassA> pa; // 在B中引用A
};
int main() {
shared_ptr<ClassA> spa = make_shared<ClassA>();
shared_ptr<ClassB> spb = make_shared<ClassB>();
spa->pb = spb;
spb->pa = spa;
return 0;
// 函数结束,思考一下:spa和spb会释放资源么?
}
左值右值
左值可以通过&
操作符取地址(左值必然有名字),其它都是右值。
右值引用:&&
。左右值一个主要区别:左值可以被修改,右值不能。
C++标准库例如:vector::push_back
之类,会对参数的对象和数据都进行复制,会造成对象内存的额外创建。
#include <iostream>
#include <memory>
#include <vector>
using std::cout;
using std::endl;
class Test
{
public:
Test(int size):sz(size) {
data = new int[sz];
cout << "Test" << endl;
}
Test(const Test &t):sz(t.sz) {
data = new int[sz];
for (int i = 0; i < sz; i++) {
data[i] = t.data[i];
}
cout << "Test copy" << endl;
}
~Test() {
delete [] data;
cout << "~Test" << endl;
}
int *data;
int sz;
};
int main()
{
std::vector<Test> v;
Test a = Test(10);
v.push_back(a);
// Test
// Test copy
// ~Test
// ~Test
return 0;
}
move:
#include <iostream>
#include <memory>
#include <vector>
using std::cout;
using std::endl;
class Test
{
public:
Test(int size):sz(size) {
data = new int[sz];
cout << "Test" << endl;
}
// copy constructor
Test(const Test &t):sz(t.sz) {
data = new int[sz];
for (int i = 0; i < sz; i++) {
data[i] = t.data[i];
}
cout << "Test copy" << endl;
}
// move constructor
Test(Test&& t):sz(t.sz) {
data = t.data;
sz = t.sz;
t.sz = 0;
t.data = nullptr;
cout << "Test move constructor" << endl;
}
~Test() {
delete [] data;
cout << "~Test" << endl;
}
int *data;
int sz;
};
int main()
{
std::vector<Test> v;
Test a = Test(10);
v.push_back(std::move(a));
// Test
// Test move constructor
// ~Test
// ~Test
return 0;
}
C++/C的内存分配,栈和堆的区别,为什么栈要快
- 栈区(stack) 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
- 堆区(heap) 就是那些由 new 分配的内存块,一般一个 new 就要对应一个 delete。一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链。堆可以动态地扩展和收缩。
- 自由存储区 就是那些由 malloc 等分配的内存块,他和堆是十分相似的,不过它是用 free 来结束自己的生命的。
- 全局区(静态区)(static)全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域data段, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域bss段。程序结束后有系统释放
- 常量存储区 存放的是常量,不允许修改。常量字符串就是放在这里的。常量字符串不能修改, 否则程序会在运行期崩溃。
docker
只是用过。
虚函数、底层机制
虚函数表和虚函数指针。

static
- 修饰全局变量,只能本文件访问。
- 静态局部变量只执行初始化一次,并且程序运行结束后才释放。
野指针
指向垃圾内存的指针,如free了,但是没有置为nullptr。
怎么找路由器地址
网关ip
new、malloc、delete,释放后调用的后果
new的时候两件事:分配内存、构造函数构造对象
delete:析构函数、释放内存。
如果用new 创建对象数组,那么只能使用对象的无参数构造函数。
new能自动计算分配空间,malloc需要计算字节数。
new是类型安全的,malloc不是(要转换类型),编译阶段找不到错误。
编译的过程
C/C++代码转化为二进制程序:
- 预处理:处理宏定义,#include,删除注释等。
- 编译:词法分析(分词)、语法分析(将单词组合成语法短语)、语义分析(类型审查、比如代码瞎写不让过)、优化生成汇编代码。
- 汇编:对汇编代码进行处理。
- 链接:静态链接、动态链接。
extern C
在C++代码中调用其他C语言代码,如C++支持函数重载,在编译的时候会将函数的参数类型也加到编译后的代码里,而C不支持函数重载,不会带上函数的参数类型。在C++里面调用C dll库的时候,加上extern C来正确使用库。
一个项目多线程版本和多进程版本之间的区别
进程是资源分配的最小单位,线程时CPU调度的最小单位。
- 数据共享
- 内存、CPU
- 创建、销毁和切换
- 可靠性:进程之间不会相互影响,线程一个挂掉整个进程挂掉。
设计模式
工厂模式
创建对象不会对客户端暴露逻辑,而是使用一个共同的接口来指向新创建的对象。
单例模式
单例
装饰模式
包装,为现有类添加功能,又不破坏原有的结构。
代理模式
可以在创建对象之后附加一些操作。
查询DNS的过程、DNS在哪一层、使用什么协议实现、为什么不使用TCP

DNS在应用层:提供域名到IP地址之间的解析服务。
DNS协议使用UDP协议,只有主DNS服务器和辅助DNS服务器数据同步时使用TCP协议。
UDP不需经过三次握手,查询域名一般返回内容不超过512字节。
tcp、ip首部字段
tcp:源端口、目标端口、序列号(发送的第一个字节的编号)、确认号(期望收到的下一个字节的编号)、TCP首部长度、校验和、窗口大小、ACK、SYN、FIN。
udp:源端口、目的端口、长度、校验和。
ip:源ip地址、目的ip地址、TTL(ip数据包可以经过的路由器数)、版本、长度。
accept发生在TCP那个阶段
accept返回是在三次握手之后,但是在三次握手之前调用会阻塞,也可以三次握手之后调用。
执行函数,得到EAGAIN的情况
比如异步发送,发送给缓冲区之后立即返回,但是可能缓冲区满了,EAGAIN。
如果你连续做read操作而没有数据可读。此时程序不会阻塞起来等待数据准备就绪返回,read函数会返回一个错误EAGAIN,提示你的应用程序现在没有数据可读请稍后再试。
epoll原理
网卡把数据写入到内存后,网卡向CPU发出一个中断信号,操作系统便得知有新数据到来。

数据到来之后:

select每次收到数据都需要重新添加等待队列然后阻塞,而且用户进程不知道哪个socket就绪了,只能遍历。
epoll使用epoll_ctl维护等待队列,epoll_wait阻塞进程。

进程的状态改变:

sock在epoll中以红黑树方式存放。

共享内存、进程通信、匿名管道
以前记录过了:链接
模板能在cpp文件写实现吗?为什么?
回答:不能分开写,但是硬要分开写,也可以,会有问题,但是可以解决。基本上是不能的意思了。
在C程序中
#include <stdio.h>
的作用仅仅是在预编译的时候得到printf
的声明,知道要去外面找这个符号。但是这个符号在哪,.h文件是不知道的,需要在编译的时候让链接器去所有目标文件里找这个符号,然后把它链接进来。
初始化列表的理解,为什么不在构造函数里面初始化成员变量?
foo(string s, int i):name(s), id(i){} ; // 初始化列表
构造函数之前,会进行参数初始化,
如:
struct Test1
{
Test1() // 无参构造函数
{
cout << "Construct Test1" << endl ;
}
Test1(const Test1& t1) // 拷贝构造函数
{
cout << "Copy constructor for Test1" << endl ;
this->a = t1.a ;
}
Test1& operator = (const Test1& t1) // 赋值运算符
{
cout << "assignment for Test1" << endl ;
this->a = t1.a ;
return *this;
}
int a ;
};
struct Test2
{
Test1 test1 ;
Test2(Test1 &t1)
{
test1 = t1 ;
}
};
会调用两次构造函数,一次赋值函数。
使用初始化列表是基于性能考虑。对于类类型,会少一次构造。一个好的原则是,能使用初始化列表的时候尽量使用初始化列表。
常量、引用类型(不能重新赋值)、没有默认构造函数的类,必须放入初始化列表。
(mmap)为什么不用read?(零拷贝的好处)
普通读写文件方式:

mmap:直接在用户空间读写页缓存。把内核空间和用户空间的虚拟地址映射到同一个物理地址。

零拷贝:CPU不需要将数据从一个内存区域复制到另一个内存区域,从而减少上下文切换及CPU的拷贝时间。
渐进式rehash
量可能比较大,短时间拷贝不完。
步骤:
- 分配空间,新旧两个哈希表同时存在
- 在字典中维护一个索引计数器变量。
- rehash期间,执行添加、删除、查找、更新操作时,除了完成指定的操作,还会把索引计数器对应的键值rehash到新表,完成后,索引+1。
- 在某个时间点,会全部rehash完,索引设置为-1,表示已经完成。
epoll边缘触发,EAGAIN
读:只要可读,就一直读,直到返回0,或者 errno = EAGAIN 写: 只要可写,就一直写,直到数据发送完,或者 errno = EAGAIN
zookeeper原理
zookeeper 约等于:文件系统+通知机制。
它维护了一个类似文件系统的数据结构:

每一个子目录项称为znode,可以增删改查。
四种类型znode:
- 持久化目录节点,客户端和zookeeper断开连接后,节点依旧存在。
- 持久化顺序编号目录节点,比上面多了一个给节点名称进行顺序编号。
- 临时目录节点。
- 临时顺序编号目录节点。
在分布式系统中,顺序号可以被用于为所有的事件进行全局排序,这样客户端可以通过顺序号推断事件的顺序。
客户端注册监听它关心的目录节点,目录节点变化后,zookeeper会通知客户端。
zookeeper用途
命名服务
通过path相互发现。
配置服务
远程动态配置。
集群管理
创建临时目录节点,然后监听父目录节点的子节点变化消息,一旦有机器挂掉,临时目录节点被删除,所有机器收到通知,加入也是如此。
选举master,所有机器创建临时顺序编号目录节点,每次选取最小的机器作为master。
分布式锁
zk会新建一个临时节点,争夺这个锁的都挂在下面,最上面的就是抢到了锁的,删除了就是释放,然后接着轮到第一个抢到锁。
无锁队列怎么实现
CAS操作,队尾,队首。