C++ interview
总结了下C++ 相关的基础知识用于面试,大部分内容从网上搜罗而来,侵删;有些内容是根据自己理解写的,如有错误请指出哦。
const
- 修饰变量,说明该变量不能被改变
- 修饰指针,分为指向常量的指针和指针常量。
指向常量的指针值指针所指地址为常量,此处值不可修改,但是可以修改指针所指的地址。
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; // 常引用
- 修饰成员函数,该成员函数内不可以修改成员变量
- const 常量引用,经常用于形参类型,即避免了拷贝,又避免了函数对值的修改;
static
- 修饰普通变量, 修改变量的生命周期和作用域,使变量存储在静态区,在main函数运行前就分配空间和初始化,如果没有初始化值,用系统默认值初始化。
- 修饰普通函数,表明函数的作用范围只在定义函数的文件内使用,多人开发项目时候,避免函数同名,可将函数定义为static
- 修饰成员变量,不用构造对象就可以访问这个成员,且多个对象只保存一个变量
- 修饰成员函数,不需要构造对象就可以访问这个成员,static成员函数内部不能访问非static的成员,包括非static的成员函数和成员变量;
引用
- 引用是变量的别名,和变量指向的是同一片内存空间,且不会重新分配空间保存这个引用。
- 引用必须初始化
- 一个变量可以有多个引用
- const &, 将引用与const对象绑定。由于const引用是为const对象取别名。
int a = 5;
int &b = a;
函数传参传指针和传引用的区别?
汇编层面看,没有区别,引用就是指针!
在C++类中,一共有8个默认函数
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继承。
- 继承后不能访问父类的私有成员。
- 不想在派生类类外直接访问继承过来的成员,又想在子类中直接访问,可以修改父类对象为protected
- 菱形继承用虚继承解决;
- 隐藏:继承后在子类实现与父类同名同参数方法,会覆盖掉父类的方法。
多态
多种状态,具体就是完成某种行为,不同的对象完成会产生不同的状态。通过函数重载(静态多态)和继承(动态多态)实现。
- 函数重载overload:通过一个类中,函数名相同,输入参数不同,返回值必须相同。
- 重写override: 指在派生类中重新对基类中的虚函数(注意是虚函数)重新实现。即函数名和参数都一样,只是函数的实现体不一样。
- 必须通过基类的指针或者引用调用虚函数; 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
- 基类的析构函数必须是虚函数,这样可以保证在用基类指针管理派生类时,析构这个对象时,先析构父类,再析构子类,如果析构函数不是虚函数,则析构过程中不会调用父类的析构函数,造成内存泄漏。
虚函数和纯虚函数
- 虚函数必须实现,纯虚函数不用父类中实现,是个接口,==0;
- final修饰虚函数,表示这个函数不能再被继承
3, 有纯虚函数的类叫抽象类,抽象类不能实例化出对象,只有派生类重写了纯虚函数,派生类才能实例化出对象。
动态绑定与静态绑定
静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
虚函数存在哪的?虚表存在哪的?
答:虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外
对象中存的不是虚表,存的是虚表指针
智能指针
shared_ptr
共享式指针,多个智能指针指向相同对象,引用计数为0时自动析构。
多线程安全?
unique_ptr
实现独占式拥有(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
弱引用指针,允许你共享但不拥有某对象,一旦最末一个拥有该对象的智能指针失去了所有权,任何weak_ptr 都会自动成空(empty)。
智能指针陷阱
- 不使用相同的内置指针值初始化(或reset)多个智能指针。
- 不delete get()返回的指针
- 不使用get()初始化或reset另一个智能指针
- 如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了
- 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。
lambda表达式
捕获列表能够捕捉上下文中的变量以供Lambda函数使用
[] 表示不捕获任何变量
[=]表示值传递方式捕获所有父作用域的变量(包括this
)
[var] 表示值传递方式捕获变量var
[&var] 表示引用传递捕捉变量var
[&] 表示引用传递方式捕捉所有父作用域的变量(包括this
)
[this]表示值传递方式捕捉当前的this
指针
[&, a, this] 表示以值传递的方式捕捉变量a
和this
,引用传递方式捕捉其它所有变量
[=, &a, &b] 表示以引用传递的方式捕捉变量a
和b
,以值传递方式捕捉其它所有变量。
Lambda表达式的优点
- 使用Lamdba表达式变得更加紧凑,结构层次更加明显、代码可读性更好
Lambda表达式的缺点
- Lamdba表达式语法比较灵活,增加了阅读代码的难度
- 对于函数复用无能为力
C++仿函数
仿函数(functor)又称为函数对象(function object)是一个能行使函数功能的类。仿函数的语法几乎和我们普通的函数调用一样,不过作为仿函数的类,都必须重载operator()
运算符,仿函数与Lamdba表达式的作用是一致的。
#include <iostream>
#include <string>
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::move将左值转换为右值
forward的作用
std::forward被称为完美转发,它的作用是保持原来的值
属性不变。啥意思呢?通俗的讲就是,如果原来的值是左值,经std::forward处理后该值还是左值;如果原来的值是右值,经std::forward处理后它还是右值。
强制类型转换运算符
static_cast
- 用于非多态类型的转换
- 不执行运行时类型检查(转换安全性不如 dynamic_cast)
- 通常用于转换数值数据类型(如 float -> int
const_cast
删除const volitile关键字
dynamic_cast
- 用于多态类型的转换
- 执行行运行时类型检查
- 只适用于指针或引用
- 对不明确的指针的转换将失败(返回 nullptr),但不引发异常
- 可以在整个类层次结构中移动指针,包括向上转换、向下转换
reinterpret_cast
- 用于位的简单重新解释
- 滥用 reinterpret_cast 运算符可能很容易带来风险。除非所需转换本身是低级别的,否则应使用其他强制转换运算符之一。
- 允许将任何指针转换为任何其他指针类型(如 char
到
int 或 One_class到
Unrelated_class 之类的转换,但其本身并不安全)也允许将任何整数类型转换为任何指针类型以及反向转换。 - reinterpret_cast 运算符不能丢掉 const、volatile 或 __unaligned 特性。
- reinterpret_cast 的一个实际用途是在哈希函数中,即,通过让两个不同的值几乎不以相同的索引结尾的方式将值映射到索引。
模板函数
隐示实例化和显示实例化
显示实例化:
template 函数返回值类型 函数名<实例化的类型>(参数列表);
template void quickSort(int *a, const int left, const int right);
快速排序
- 每次循环找到第一个数的位置,最左边是比temp小的数,最右边是比temp大的数,不断重复循环。
- 可以用这个方法解决TOPK的问题,在一个无序数组中找到第K个大的值,还是按照快排方式循环,直到temp的index等于K时返回,此时temp值就是第K大的值。
- 快排实现
template <typename T>
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++程序编译的内存分配情况
一个 C、C++程序编译时内存分为 5 大存储区:堆区、栈区、全局区、文字常量区、程序代码区。
C、C++中内存分配方式可以分为三种:
- 从静态存储区域分配:
内存在程序编译时就已经分配好,这块内存在程序的整个运行期间都存在。速度快、不容易出错,因为有系统会善后。例如全局变量,static 变量等。 - 在栈上分配:
在执行函数时,函数内局部变量的存储单元都在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 - 从堆上分配:
即动态内存分配。程序在运行的时候用 malloc 或 new 申请任意大小的内存,程序员自己负责在何时用 free 或 delete 释放内存。动态内存的生存期由程序员决定,使用非常灵活。如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏,另外频繁地分配和释放不同大小的堆空间将会产生堆内碎块。
C++ 性能优化
1 计算
- 不要混淆单精度和双精度的计算,混合计算会带来额外的开销;
- 浮点除法很慢,可以推导公式用乘法加快速度;
- 部分变量修改为const常量,提高复用性
- const,enum,inline代替define
2 函数调用
- 减少函数直接调用,偏向使用静态链接而不是动态链接;
- 用inline代替普通函数
- 函数传参使用const 引用,减少变量的构造
3 分支
- 消除条件分支,简单的可以用三木运算代替
- 循环展开,减少循环
- 减少循环内的if else
- 多的if else可以用switch case 代替
- if else 最容易出现的分支放在最前面
4 继承
尽量少用或者不用多继承和虚继承
多线程同步的四种方式
对于多线程程序来说,同步是指在一定的时间内只允许某一个线程来访问某个资源。而在此时间内,不允许其他的线程访问该资源。可以通过互斥锁(Mutex)、条件变量(condition variable)、读写锁(reader-writer lock)、信号量(semaphore)来同步资源。
1 互斥锁(Mutex)
互斥量是最简单的同步机制,即互斥锁。多个进程(线程)均可以访问到一个互斥量,通过对互斥量加锁,从而来保护一个临界区,防止其它进程(线程)同时进入临界区,保护临界资源互斥访问。
- mutex.lock mutex.unlock()
- lock_guard lg(mutex) // 加锁,析构自动释放,不用手动释放
- unique_guard un(mutex) // 加锁,保护临界资源
… 临界资源
un.unlock(); 释放
2 条件变量(condition variable)
条件变量适合多个进程(线程)等待同一事件发生,然后去干某事。
std::condition_variable : 配合std::unique_lock进行wait
std::condition_variable_any : 和任意锁类型搭配使用,效率低
功能函数:
wait() 如果条件不满足,则释放锁,阻塞,等条件满足后获取锁,继续运行。
notify_one() 通知一个等待条件的线程
notify_all() 通知所有等待条件的线程
用法:
std::list<T> m_queue;
std::mutex m_mutex;
std::condition_variable m_notEmpty;
void Put(const T& x) {
std::lock_guard<std::mutex> locker(m_mutex);
m_queue.push_back(x);
m_notEmpty.notify_one();//激活一个等待线程,notify_all() 激活所有
}
void Take(T& x) {
std::unique_lock<std::mutex> 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还被占着,其他线程还没法执行,就永远也没法唤醒这个线程,就死锁了。
3 读写锁(reader-writer lock)
读写锁适合于使用在读操作多,写操作少的情况,比如数据库。读写锁读锁可以同时加很多,但是写锁是互斥的。当有进程或者线程要写时,必须等待所有的读进程或者线程都释放自己的读锁方可以写。数据库很多时候可能只是做一些查询。
4 信号量(semaphore)
在生产者消费者模型中,对任务数量的记录就可以使用信号量来做。可以理解为带计数的条件变量。当信号量的值小于0时,工作进程或者线程就会阻塞,等待物品到来。当生产者生产一个物品,会将信号量值加1操作。 这是会唤醒在信号量上阻塞的进程或者线程,它们去争抢物品。
constexpr 限定符
常量表达式
常量表达式:指值不会改变并且在编译过程就能得到结果的表达式;字面值、用常量表达式初始化的const对象也是常量表达式。
字面值类型:算术类型、引用和指针都属于字面值类型,自定义类、IO库,string类型则不属于字面值类型,不能被定义成constexpr;
TCP
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协 议,其传输的单位是报文段。
特征: 面向连接 只能点对点(一对一)通信 可靠交互 全双工通信 面向字节流
三次握手
TCP三次握手是浏览器和服务器建立连接的方式,目的是为了使二者能够建立连接,便于后续的数据交互传输。
第一次握手:浏览器向服务器发起建立连接的请求
第二次握手:服务器告诉浏览器,我同意你的连接请求,同时我也向你发起建立连接的请求
第三次握手:浏览器也告诉服务器,我同意建立连接。
至此,双方都知道对方同意建立连接,并准备好了进行数据传输,也知道对方知道自己的情况。接下来就可以传输数据了
第 1 次握手
第 1 次握手建立连接时,客户端向服务器发送 SYN 报文(SEQ=x,SYN=1),并进入 SYN_SENT 状态,等待服务器确认。
第 2 次握手
第 2 次握手实际上是分两部分来完成的,即 SYN+ACK(请求和确认)报文。
- 服务器收到了客户端的请求,向客户端回复一个确认信息(ACK=x+1)。
- 服务器再向客户端发送一个 SYN 包(SEQ=y)建立连接的请求,此时服务器进入 SYN_RECV 状态
第 3 次握手
第 3 次握手,是客户端收到服务器的回复(SYN+ACK 报文)。此时,客户端也要向服务器发送确认包(ACK)。此包发送完毕客户端和服务器进入 ESTABLISHED 状态,完成 3 次握手。
四次挥手
- 客户端发送 FIN 给服务器,说明客户端不必发送数据给服务器了(请求释放从客户端到服务器的连接)
- 服务器接收到客户端发的 FIN,并回复 ACK 给客户端(同意释放从客户端到服务器的连接);
- 客户端收到服务端回复的 ACK,此时从客户端到服务器的连接已释放(但服务端到客户端的连接还未释放,并且 客户端还可以接收数据);
- 服务端继续发送之前没发完的数据给客户端;
- 服务端发送 FIN+ACK 给客户端,说明服务端发送完了数据(请求释放从服务端到客户端的连接,就算没收到客户 端的回复,过段时间也会自动释放);
- 客户端收到服务端的 FIN+ACK,并回复 ACK 给客户端(同意释放从服务端到客户端的连接);
- 服务端收到客户端的 ACK 后,释放从服务端到客户端的连接
TCP:状态控制码(Code,Control Flag)
占 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)关闭套接字
- 创建socket
void *fd = NULL;
fd = sock_reg(AF_INET, SOCK_STREAM, 0, NULL, NULL);
if(NULL == fd)
{
printf(“sock_reg fail\n”);
}
- 设置socket属性(可选)
unsigned int opt = 1;
if(sock_setsockopt(fd, SOL_SOCKET, SO_REUSEADDR ,&opt, sizeof(opt)) < 0)
{
printf(“sock_setsockopt fail\n”);
}
- 绑定IP地址、端口号等信息(可选)
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”);
}
- 等待客户端连接 accept
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);
- 关闭socket
if(fd)
{
flag = 1;
sock_set_quit(fd);
while(flag) //等待sock_accept退出后再释放socket,防止释放socket后还在使用。
{
os_time_dly(20);
}
sock_unreg(fd);
fd = NULL;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
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;
}
客户端构建
- 创建socket套接字
- 绑定服务器IP和端口
- connect
- 读/写
- 关闭socket
#include <stdio.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */ //man socket
#include <sys/socket.h>
#include <sys/socket.h> // man 7 ip
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <sys/socket.h> //man 3 inet_addr
#include <netinet/in.h>
#include <arpa/inet.h> //man 2 read
#include <unistd.h>
#include <ctype.h>
#include <stdlib.h>
#define SIZE 1024
#define SERV_IP "0"
#define SERV_PORT 6666
int 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 0
struct sockaddr_in saddr ;
memset(&saddr, 0, sizeof(saddr)); // bzero(&saddr, sizeof(saddr));
saddr.sin_family = AF_INET; //IPV4 协议
saddr.sin_port = htons(6666);//端口号 :1024-49151
saddr.sin_addr.s_addr= inet_addr("192.168.16.100");
#else
struct 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和端口a
socklen_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<count; i++)
{
recvbuf[i] = toupper(recvbuf[i]); //将单个字符转化大写
}
//write(connfd, "ok", 2);
write(connfd, recvbuf, count);
}
//6、关闭套结字退出
close(connfd);
}
close(listenfd);
return 0;
}
TCP 黏包问题
- 发送定长包。如果每个消息的大小都是一样的,那么在接收对等方只要累计接收数据,直到数据等于一个定长的数值就将它作为一个消息。包头加上包体长度。包头是定长的 4 个字节,说明了包体的长度。接收对等方先接收包头长度,依据包头长度来接收包体。
- 在数据包之间设置边界,如添加特殊符号 \r\n 标记。FTP 协议正是这么做的。但问题在于如果数据正文中也含有 \r\n,则会误判为消息的边界。
- 使用更加复杂的应用层协议。
UDP
UDP UDP(User Datagram Protocol,用户数据报协议)是 OSI(Open System Interconnection 开放式系统互联) 参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务,其传输的单位是用户数据报。
特征:
1 无连接
2 尽最大努力交付
3 面向报文
4 没有拥塞控制
5 支持一对一、一对多、多对一、多对多的交互通信
6 首部开销小
服务端构建
#include <stdio.h>
#include <winsock2.h>
#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 <stdio.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib") //加载 ws2_32.dll
#define BUF_SIZE 100
int main(){
//初始化DLL
WSADATA 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 与 UDP 的区别
- TCP 面向连接,UDP 是无连接的;
- TCP 提供可靠的服务,也就是说,通过 TCP 连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP 尽最大努力交付,即不保证可靠交付
- TCP 的逻辑通信信道是全双工的可靠信道;UDP 则是不可靠信道
- 每一条 TCP 连接只能是点到点的;UDP 支持一对一,一对多,多对一和多对多的交互通信
- TCP 面向字节流(可能出现黏包问题),实际上是 TCP 把数据看成一连串无结构的字节流;UDP 是面向报文的(不会出现黏包问题)
- UDP 没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如 IP 电话,实时视频会议等)
- TCP 首部开销20字节;UDP 的首部开销小,只有 8 个字节
TCP 为什么要进行四次挥手?/ 为什么 TCP 建立连接需要三次,而释放连接则需要四次?
因为 TCP 是全双工模式,客户端请求关闭连接后,客户端向服务端的连接关闭(一二次挥 手),服务端继续传输之前没传完的数据给客户端(数据传输),服务端向客户端的连接关闭(三四 次挥手)。所以 TCP 释放连接时服务器的 ACK 和 FIN 是分开发送的(中间隔着数据传输),而 TCP 建立连接时服务器的 ACK 和 SYN 是一起发送的(第二次握手),所以 TCP 建立连接需要三次,而释 放连接则需要四次。
为什么 TCP 连接时可以 ACK 和 SYN 一起发送,而释放时则 ACK 和 FIN 分开发送呢? (ACK 和 FIN 分开是指第二次和第三次挥手)
因为客户端请求释放时,服务器可能还有数据需要传输给客户端,因此服务端要先响应客 户端 FIN 请求(服务端发送 ACK),然后数据传输,传输完成后,服务端再提出 FIN 请求(服务端发 送 FIN);而连接时则没有中间的数据传输,因此连接时可以 ACK 和 SYN 一起发送。
为什么客户端释放最后需要 TIME-WAIT 等待 2MSL 呢?
- 为了保证客户端发送的最后一个 ACK 报文能够到达服务端。若未成功到达,则服务端超时重传 FIN+ACK 报文 段,客户端再重传 ACK,并重新计时。
- 防止已失效的连接请求报文段出现在本连接中。TIME-WAIT 持续 2MSL 可使本连接持续的时间内所产生的所有报 文段都从网络中消失,这样可使下次连接中不会出现旧的连接报文段。
进程和线程的区别
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信号,表示立即结束进程。