总结了下C++ 相关的基础知识用于面试,大部分内容从网上搜罗而来,侵删;有些内容是根据自己理解写的,如有错误请指出哦。
指向常量的指针值指针所指地址为常量,此处值不可修改,但是可以修改指针所指的地址。
int a = 10;
int b = 15;
const int *p;
p = &a;
*p = 20; //报错,不可修改指针所指向的值
p = &b; //通过,可修改指针所指向的地址
指针常量
int a = 10;
int b = 15;
int * const p = &a;
p = &b; //报错,不可修改指针所指向的地址
指向常量的常指针
int a = 10;
int b = 15;
const int * const p = &a;
p = &b; //报错,不可修改指针所指向的地址
*p = 20; //报错,不可以修改指针所指向的值
A b; // 普通对象,可以调用全部成员函数
const A a; // 常对象,只能调用常成员函数、更新常成员变量
const A *p = &a; // 常指针
const A &q = a; // 常引用
int a = 5;
int &b = a;
函数传参传指针和传引用的区别?
汇编层面看,没有区别,引用就是指针!
class A
{
public:A(); // 默认构造函数~A(); // 默认析构函数A(const A&); // 默认拷贝构造函数A& operator = (const A&); // 默认重载赋值运算符函数A* operator & (); // 默认取地址运算符函数const A* operator & () const; // 默认重载取地址运算符const函数A(A&&); // 默认移动构造函数A& operator = (const A&&); // 默认重载移动赋值操作符函数
};
把抽象的事物封装成一个类,根据需要去构造对象。
继承父类的属性和方法,在父类的基础上进行功能扩展。分为public,protected,privated继承。
多种状态,具体就是完成某种行为,不同的对象完成会产生不同的状态。通过函数重载(静态多态)和继承(动态多态)实现。
静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
答:虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外
对象中存的不是虚表,存的是虚表指针
共享式指针,多个智能指针指向相同对象,引用计数为0时自动析构。
多线程安全?
实现独占式拥有(exclusive ownership)或严格拥有(strict ownership)概念,保证同一时间内只有一个智能指针可以指向该对象。
unique_ptr没有拷贝构造函数,不能拷贝,只能移动unique_ptr。这意味着,内存资源所有权将转移到另一 unique_ptr,并且原始 unique_ptr 不再拥有此资源;
unique_ptr ptr_1(new ClassTest());
unique_ptr ptr_2 = make_unique();
unique_ptr ptr_3 = std::move(ptr_2); // 移动
弱引用指针,允许你共享但不拥有某对象,一旦最末一个拥有该对象的智能指针失去了所有权,任何weak_ptr 都会自动成空(empty)。
捕获列表能够捕捉上下文中的变量以供Lambda函数使用
[] 表示不捕获任何变量
[=]表示值传递方式捕获所有父作用域的变量(包括this)
[var] 表示值传递方式捕获变量var
[&var] 表示引用传递捕捉变量var
[&] 表示引用传递方式捕捉所有父作用域的变量(包括this)
[this]表示值传递方式捕捉当前的this指针
[&, a, this] 表示以值传递的方式捕捉变量a和this,引用传递方式捕捉其它所有变量
[=, &a, &b] 表示以引用传递的方式捕捉变量a和b,以值传递方式捕捉其它所有变量。
仿函数(functor)又称为函数对象(function object)是一个能行使函数功能的类。仿函数的语法几乎和我们普通的函数调用一样,不过作为仿函数的类,都必须重载operator()运算符,仿函数与Lamdba表达式的作用是一致的。
#include
#include
using namespace std;class Functor
{
public:void operator() (const string& str) const{cout << str << endl;}
};int main()
{Functor myFunctor;myFunctor("Hello world!");return 0;
}
右值引用主要用于移动语义和完美转发,其中前者需要有修改右值的权限
可以用std::move将右值转换为左值
std::forward被称为完美转发,它的作用是保持原来的值属性不变。啥意思呢?通俗的讲就是,如果原来的值是左值,经std::forward处理后该值还是左值;如果原来的值是右值,经std::forward处理后它还是右值。
删除const volitile关键字
到int 或 One_class 到Unrelated_class 之类的转换,但其本身并不安全)也允许将任何整数类型转换为任何指针类型以及反向转换。隐示实例化和显示实例化
显示实例化:
template 函数返回值类型 函数名<实例化的类型>(参数列表);
template void quickSort(int *a, const int left, const int right);
template
void quickSort(T *a, const int left, const int right) {if(left >= right) {return;}int i = left, j = right;T temp = a[left];while (i < j) {while(i < j && a[j] >= temp) {j--;}if(i < j) {a[i] = a[j];i++;}while(i < j && a[i] < temp) {i++;}if(i < j) {a[j] = a[i];j--;}a[i] = temp;quickSort(a, left, i - 1);quickSort(a, i + 1, right);}
}
一个 C、C++程序编译时内存分为 5 大存储区:堆区、栈区、全局区、文字常量区、程序代码区。
C、C++中内存分配方式可以分为三种:
尽量少用或者不用多继承和虚继承
对于多线程程序来说,同步是指在一定的时间内只允许某一个线程来访问某个资源。而在此时间内,不允许其他的线程访问该资源。可以通过互斥锁(Mutex)、条件变量(condition variable)、读写锁(reader-writer lock)、信号量(semaphore)来同步资源。
互斥量是最简单的同步机制,即互斥锁。多个进程(线程)均可以访问到一个互斥量,通过对互斥量加锁,从而来保护一个临界区,防止其它进程(线程)同时进入临界区,保护临界资源互斥访问。
条件变量适合多个进程(线程)等待同一事件发生,然后去干某事。
std::condition_variable : 配合std::unique_lock进行wait
std::condition_variable_any : 和任意锁类型搭配使用,效率低
功能函数:
wait() 如果条件不满足,则释放锁,阻塞,等条件满足后获取锁,继续运行。
notify_one() 通知一个等待条件的线程
notify_all() 通知所有等待条件的线程
用法:
std::list m_queue;
std::mutex m_mutex;
std::condition_variable m_notEmpty;void Put(const T& x) {std::lock_guard locker(m_mutex);m_queue.push_back(x);m_notEmpty.notify_one();//激活一个等待线程,notify_all() 激活所有
}void Take(T& x) {std::unique_lock locker(m_mutex); //因为wait会解锁,不能用lock_guard加锁m_notEmpty.wait(m_mutex, [this] {return !m_queue.empty();});x = m_queue.front();m_queue.pop_front();
}
为什么条件变量中要有互斥锁呢?
总而言之,就是在做cond_wait的时候,需要先解锁释放资源,让其他线程有机会操作条件变量,然后用wait阻塞当前线程,待到被唤醒时再重新加锁。但是为了防止在解锁和wait之间条件变量被修改,解锁和wait应该是一个原子操作。为了让解锁和wait是原子的,他会自动完成原子的释放锁和阻塞,以及被唤醒后的自动加锁。
那么为什么不能先wait阻塞再释放锁呢?因为wait之后本线程已经阻塞并等待唤醒了,而它还没有释放锁,cpu还被占着,其他线程还没法执行,就永远也没法唤醒这个线程,就死锁了。
读写锁适合于使用在读操作多,写操作少的情况,比如数据库。读写锁读锁可以同时加很多,但是写锁是互斥的。当有进程或者线程要写时,必须等待所有的读进程或者线程都释放自己的读锁方可以写。数据库很多时候可能只是做一些查询。
在生产者消费者模型中,对任务数量的记录就可以使用信号量来做。可以理解为带计数的条件变量。当信号量的值小于0时,工作进程或者线程就会阻塞,等待物品到来。当生产者生产一个物品,会将信号量值加1操作。 这是会唤醒在信号量上阻塞的进程或者线程,它们去争抢物品。
常量表达式
常量表达式:指值不会改变并且在编译过程就能得到结果的表达式;字面值、用常量表达式初始化的const对象也是常量表达式。
字面值类型:算术类型、引用和指针都属于字面值类型,自定义类、IO库,string类型则不属于字面值类型,不能被定义成constexpr;
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协 议,其传输的单位是报文段。
特征: 面向连接 只能点对点(一对一)通信 可靠交互 全双工通信 面向字节流
TCP三次握手是浏览器和服务器建立连接的方式,目的是为了使二者能够建立连接,便于后续的数据交互传输。
第一次握手:浏览器向服务器发起建立连接的请求
第二次握手:服务器告诉浏览器,我同意你的连接请求,同时我也向你发起建立连接的请求
第三次握手:浏览器也告诉服务器,我同意建立连接。
至此,双方都知道对方同意建立连接,并准备好了进行数据传输,也知道对方知道自己的情况。接下来就可以传输数据了
第 1 次握手建立连接时,客户端向服务器发送 SYN 报文(SEQ=x,SYN=1),并进入 SYN_SENT 状态,等待服务器确认。
第 2 次握手实际上是分两部分来完成的,即 SYN+ACK(请求和确认)报文。
第 3 次握手,是客户端收到服务器的回复(SYN+ACK 报文)。此时,客户端也要向服务器发送确认包(ACK)。此包发送完毕客户端和服务器进入 ESTABLISHED 状态,完成 3 次握手。
占 6 比特,含义如下:
URG:紧急比特(urgent),当 URG=1 时,表明紧急指针字段有效,代表该封包为紧急封包。它告诉系统此报 文段中有紧急数据,应尽快传送(相当于高优先级的数据), 且上图中的 Urgent Pointer 字段也会被启用
ACK:确认比特(Acknowledge)。只有当 ACK=1 时确认号字段才有效,代表这个封包为确认封包。当 ACK= 0 时,确认号无效
PSH:(Push function)若为 1 时,代表要求对方立即传送缓冲区内的其他对应封包,而无需等缓冲满了才送
RST:复位比特(Reset),当 RST=1 时,表明 TCP 连接中出现严重差错(如由于主机崩溃或其他原因),必须释 放连接,然后再重新建立运输连接。
SYN:同步比特(Synchronous),SYN 置为 1,就表示这是一个连接请求或连接接受报文,通常带有 SYN 标志的 封包表示『主动』要连接到对方的意思。
FIN:终止比特(Final),用来释放一个连接。当 FIN=1 时,表明此报文段的发送端的数据已发送完毕,并要求释放 运输连接。
(1)创建套接字----->socket() 正确返回:监听套接字 错误返回:-1
(2)套接字绑定------>bind() 绑定核心:IP地址与PORT端口
(3)监听套接字 listen
(4)建立链接请求 accept
(5)读写
(6)关闭套接字
void *fd = NULL;
fd = sock_reg(AF_INET, SOCK_STREAM, 0, NULL, NULL);
if(NULL == fd)
{printf(“sock_reg fail\n”);
}
unsigned int opt = 1;
if(sock_setsockopt(fd, SOL_SOCKET, SO_REUSEADDR ,&opt, sizeof(opt)) < 0)
{printf(“sock_setsockopt fail\n”);
}
struct sockaddr_in local_addr;local_addr.sin_family = AF_INET;
local_addr.sin_addr.s_addr = htonl(INADDR_ANY)
local_addr.sin_port = htons(32768);if (sock_bind(fd, (struct sockaddr *)&local_addr), sizeof(local_addr))
{printf(“sock_bind fail\n”);
}
if (sock_listen(fd, 3))
{printf(“sock_listen fail\n”);
}
void *c_fd = NULL;
int flag = 1;
struct sockaddr_in c_addr;
socklen_t len = sizeof(struct sockaddr_in);c_fd = sock_accept(fd, (struct sockaddr*)&c_addr, &len, NULL, NULL);
flag = 0;
if(c_fd == NULL)
{printf(“sock_accept fail\n”);
}
char buf[1024] = {0};
int recv_len = sock_recv(fd, buf, sizeof(buf), 0);
if(recv_len > 0)
{printf(“received %d Bytes, data : %s\n”, recv_len, buf);
}
char *send_buf = “123456”;
int send_len = sock_send(fd, send_buf, strlen(send_buf), 0);
if(fd)
{flag = 1;sock_set_quit(fd);while(flag) //等待sock_accept退出后再释放socket,防止释放socket后还在使用。{os_time_dly(20);}sock_unreg(fd);fd = NULL;
}
#include
#include
#include
#include
#include
#include
#include
#include int main(int argc, char *argv[])
{ int sockfd=socket(AF_INET,SOCK_STREAM,0);//创建套接字,第一个参数为:协议族,第二个参数:套接字类型,第三个参数:0代表只存在一个协议来支持套接字类型if(sockfd==-1)//错误返回{perror("socket");exit(-1); }struct sockaddr_in ser;//定义ip地址转化的相关结构体ser.sin_family=AF_INET;//初始化族ser.sin_port=htons(8989);//初始化端口---1024~65535都可以ser.sin_addr.s_addr=inet_addr("192.168.22.245");//初始化ip地址if(-1==bind(sockfd,(struct sockaddr *)&ser,sizeof(ser)))//绑定套接字{perror("bind");exit(-1);}printf("bind success\n");if(listen(sockfd,5)==-1)//监听套接字{perror("listen");exit(-1);}int connfd=accept(sockfd,NULL,NULL);//接受服务器/客户端连接请求while(1){char buf[24]={0};//定义缓冲区大小if(connfd==-1){perror("accept");exit(-1);}read(connfd,buf,24);//读取printf("%s\n",buf);//输出printf("accept success\n");}close(connfd);//关闭监听套接字close(sockfd);//关闭套接字return 0;
}
#include
#include #include /* See NOTES */ //man socket
#include
#include // man 7 ip
#include
#include /* superset of previous */#include //man 3 inet_addr
#include
#include //man 2 read
#include
#include
#include #define SIZE 1024
#define SERV_IP "0"
#define SERV_PORT 6666int main(int argc,const char *argv[])
{int listenfd; //用于保存监听套结字int connfd ; //用于通信的套结字int ret; char recvbuf[SIZE] = {0}; //1、创建套结字 socket listenfd = socket(AF_INET, SOCK_STREAM, 0); //AF_INET:IPV4协议 SOCK_STREAM:流式套结字if(-1 == listenfd){perror("socket");return -1;}printf("socket %d ok\n", listenfd); ////填充ip等信息到通用ip结构体#if 0struct sockaddr_in saddr ;memset(&saddr, 0, sizeof(saddr)); // bzero(&saddr, sizeof(saddr));saddr.sin_family = AF_INET; //IPV4 协议saddr.sin_port = htons(6666);//端口号 :1024-49151saddr.sin_addr.s_addr= inet_addr("192.168.16.100"); #elsestruct sockaddr_in saddr = { .sin_family = AF_INET, .sin_port = htons(SERV_PORT), .sin_addr.s_addr = inet_addr(SERV_IP) }; #endif//优化2:设置套结字属性 端口重用 setsockopt(); int opt = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); //2、绑定ip和端口asocklen_t slen = sizeof(saddr); ret = bind(listenfd, (struct sockaddr* )&saddr, slen); if(-1 == ret){perror("bind");return -1;}printf("bind ok\n");//3、监听ret = listen(listenfd, 8); if(-1 == ret){perror("listen");return -1;}printf("listen ok, wait for connect...\n");//优化1:循环监听客户端 while(1){//4、处理客户端请求#if 0//accept之后 监听套结字listenfd 转接 为新的 通信套结字 connfd 使用connfd = accept(listenfd, NULL, NULL); //不关心客户端的ip和端口printf("had client connect%d\n", connfd); #else//优化 3:关心客户端ip和端口了并打印struct sockaddr_in caddr = {0};// memset(caddr, 0, sizeof(caddr)); socklen_t clen = sizeof(caddr); connfd = accept(listenfd, (struct sockaddr *)&caddr, &clen); if(connfd == -1){perror("accept");return -1;}printf("client(%s:%d) had connected success\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port) ); #endif //5、通信while(1){memset(recvbuf, 0, sizeof(recvbuf));int count = read(connfd, recvbuf, sizeof(recvbuf));if(-1 == count){perror("read");return -1;}else if(0 == count){printf("client quit\n");break; }printf("recv:%s\n",recvbuf); //判断客户端发来的指令 做出响应if( 0 == strncmp(recvbuf, "sl", 2) ){system("sl");}else if(0 == strncmp(recvbuf, "xcowsay", 7)){system("xcowsay 爱 老虎油!");}//int i;for(i=0; i
TCP 是一个基于字节流的传输服务(UDP 基于报文的),“流” 意味着 TCP 所传输的数据是没有边
界的。所以可能会出现两个数据包黏在一起的情况。
UDP UDP(User Datagram Protocol,用户数据报协议)是 OSI(Open System Interconnection 开放式系统互联) 参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务,其传输的单位是用户数据报。
特征:
1 无连接
2 尽最大努力交付
3 面向报文
4 没有拥塞控制
5 支持一对一、一对多、多对一、多对多的交互通信
6 首部开销小
#include
#include
#pragma comment (lib, "ws2_32.lib") //加载 ws2_32.dll
#define BUF_SIZE 100
int main(){WSADATA wsaData;WSAStartup( MAKEWORD(2, 2), &wsaData);//创建套接字SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0);//绑定套接字struct sockaddr_in servAddr;memset(&servAddr, 0, sizeof(servAddr)); //每个字节都用0填充servAddr.sin_family = PF_INET; //使用IPv4地址servAddr.sin_addr.s_addr = htonl(INADDR_ANY); //自动获取IP地址servAddr.sin_port = htons(1234); //端口bind(sock, (SOCKADDR*)&servAddr, sizeof(SOCKADDR));//接收客户端请求SOCKADDR clntAddr; //客户端地址信息int nSize = sizeof(SOCKADDR);char buffer[BUF_SIZE]; //缓冲区while(1){int strLen = recvfrom(sock, buffer, BUF_SIZE, 0, &clntAddr, &nSize);sendto(sock, buffer, strLen, 0, &clntAddr, nSize);}closesocket(sock);WSACleanup();return 0;
}
#include
#include
#pragma comment(lib, "ws2_32.lib") //加载 ws2_32.dll
#define BUF_SIZE 100
int main(){//初始化DLLWSADATA wsaData;WSAStartup(MAKEWORD(2, 2), &wsaData);//创建套接字SOCKET sock = socket(PF_INET, SOCK_DGRAM, 0);//服务器地址信息struct sockaddr_in servAddr;memset(&servAddr, 0, sizeof(servAddr)); //每个字节都用0填充servAddr.sin_family = PF_INET;servAddr.sin_addr.s_addr = inet_addr("127.0.0.1");servAddr.sin_port = htons(1234);//不断获取用户输入并发送给服务器,然后接受服务器数据struct sockaddr fromAddr;int addrLen = sizeof(fromAddr);while(1){char buffer[BUF_SIZE] = {0};printf("Input a string: ");gets(buffer);sendto(sock, buffer, strlen(buffer), 0, (struct sockaddr*)&servAddr, sizeof(servAddr));int strLen = recvfrom(sock, buffer, BUF_SIZE, 0, &fromAddr, &addrLen);buffer[strLen] = 0;printf("Message form server: %s\n", buffer);}closesocket(sock);WSACleanup();return 0;
}
因为 TCP 是全双工模式,客户端请求关闭连接后,客户端向服务端的连接关闭(一二次挥 手),服务端继续传输之前没传完的数据给客户端(数据传输),服务端向客户端的连接关闭(三四 次挥手)。所以 TCP 释放连接时服务器的 ACK 和 FIN 是分开发送的(中间隔着数据传输),而 TCP 建立连接时服务器的 ACK 和 SYN 是一起发送的(第二次握手),所以 TCP 建立连接需要三次,而释 放连接则需要四次。
因为客户端请求释放时,服务器可能还有数据需要传输给客户端,因此服务端要先响应客 户端 FIN 请求(服务端发送 ACK),然后数据传输,传输完成后,服务端再提出 FIN 请求(服务端发 送 FIN);而连接时则没有中间的数据传输,因此连接时可以 ACK 和 SYN 一起发送。
1.根本区别:进程是操作系统进行资源分配的最小单元,线程是操作系统进行运算调度的最小单元。
2.从属关系不同:进程中包含了线程,线程属于进程。
3.开销不同:进程的创建、销毁和切换的开销都远大于线程。
4.拥有资源不同:每个进程有自己的内存和资源,一个进程中的线程会共享这些内存和资源。
5.控制和影响能力不同:子进程无法影响父进程,而子线程可以影响父线程,如果主线程发生异常会影响其所在进程和子线程。
6.CPU利用率不同:进程的CPU利用率较低,因为上下文切换开销较大,而线程的CPU的利用率较高,上下文的切换速度快。
7.操纵者不同:进程的操纵者一般是操作系统,线程的操纵者一般是编程人员。
管道
半双工,一条管道只能一个进程写,一个进程读
消息队列
管道的通信方式效率是低下的,不适合进程间频繁的交换数据。这个问题,消息队列的通信方式就可以解决。A进程往消息队列写入数据后就可以正常返回,B进程需要时再去读取就可以了,效率比较高。
而且,数据会被分为一个一个的数据单元,称为消息体,消息发送方和接收方约定好消息体的数据类型,不像管道是无格式的字节流类型,这样的好处是可以边发送边接收,而不需要等待完整的数据。
但是也有缺点,每个消息体有一个最大长度的限制,并且队列所包含消息体的总长度也是有上限的,这是其中一个不足之处。
另一个缺点是消息队列通信过程中存在用户态和内核态之间的数据拷贝问题。进程往消息队列写入数据时,会发送用户态拷贝数据到内核态的过程,同理读取数据时会发生从内核态到用户态拷贝数据的过程。
共享内存
共享内存解决了消息队列存在的内核态和用户态之间数据拷贝的问题。
现代操作系统对于内存管理采用的是虚拟内存技术,也就是说每个进程都有自己的虚拟内存空间,虚拟内存映射到真实的物理内存。共享内存的机制就是,不同的进程拿出一块虚拟内存空间,映射到相同的物理内存空间。这样一个进程写入的东西,另一个进程马上就能够看到,不需要进行拷贝。
socket
信号量
当使用共享内存的通信方式,如果有多个进程同时往共享内存写入数据,有可能先写的进程的内容被其他进程覆盖了。
因此需要一种保护机制,信号量本质上是一个整型的计数器,用于实现进程间的互斥和同步。
信号量代表着资源的数量,操作信号量的方式有两种:
P操作:这个操作会将信号量减一,相减后信号量如果小于0,则表示资源已经被占用了,进程需要阻塞等待;如果大于等于0,则说明还有资源可用,进程可以正常执行。
V操作:这个操作会将信号量加一,相加后信号量如果小于等于0,则表明当前有进程阻塞,于是会将该进程唤醒;如果大于0,则表示当前没有阻塞的进程。
信号
在Linux中,为了响应各种事件,提供了几十种信号,可以通过kill -l命令查看。
如果是运行在shell终端的进程,可以通过键盘组合键来给进程发送信号,例如使用Ctrl+C产生SIGINT信号,表示终止进程。
如果是运行在后台的进程,可以通过命令来给进程发送信号,例如使用kill -9 PID产生SIGKILL信号,表示立即结束进程。