C/C++面试笔试知识点总结

C/C++面试笔试知识点总结

    • 1、const关键字的作用?(变量,参数,返回值)
    • 2、什么是死锁?
    • 3、造成死锁的4个必要条件?
    • 4、如何避免死锁?
    • 5、static关键字作用?
    • 6、c/c++中内存可以划分为几个部分?
    • 7、new 和 malloc的区别?
    • 8、计算机内部如何存储浮点数?
    • 9、什么是虚函数?
    • 10、什么是纯虚函数?
    • 11、什么是抽象类?
    • 12、vector和list的区别?
    • 13、空类创建时会自带哪些函数?
    • 14、指针和引用的区别?
    • 15、gcc程序编译过程?
    • 16、什么是内存泄漏?采用哪些方法来避免和减少这类错误?
    • 17、结构体对齐问题
    • 18、重载overload,覆盖override,隐藏overwrite,这三者之间的区别
    • 19、c++代码如何调用c语言代码?
    • 20、什么是野指针?
    • 21、栈溢出的原因以及解决方法?
    • 22、宏和内联函数的区别?
    • 23、类的静态成员变量和静态成员函数各有哪些特性?
    • 24、指针数组和数组指针的区别?
    • 25、虚函数表
    • 26、什么是多态?
    • 27、C++的四种强制转换
    • 28、sizeof 和 strlen 的区别?
    • 29、单链表反转
    • 30、进程和线程的区别?
    • 31、TCP/IP网络模型
    • 32、tcp协议
    • 33、udp协议
    • 34、tcp协议udp协议区别?
    • 35、进程间的通信方式
    • 36、技术面试基础知识总结(强烈推荐)
    • 37、C++ 中 struct 和 class区别?
    • 38、TCP 黏包问题
    • 39、socket中TCP的三次握手?
    • 40、socket的基本操作
    • 41、子类析构时要调用父类的析构函数吗?
    • 42、int id[sizeof(unsigned long)];这个对吗?为什么?
    • 43、typedef 和 define 有什么区别
    • 44、为什么free()释放内存的时候不需要指定内存的大小?
    • 45、STL常用标准库容器
    • 46、信号与槽的优缺点?
    • 47、说说C++智能指针?
    • 48、http和https有什么区别?
    • 48、单核cpu多线程有必要吗?
    • 49、关于*P++的问题

1、const关键字的作用?(变量,参数,返回值)

1)欲阻止一个变量被改变,可使用const,在定义该const变量时,需先初始化,以后就没有机会改变他了;
2)对指针而言,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;
3)在一个函数声明中,const可以修饰形参表明他是一个输入参数,在函数内部不可以改变其值;
4)对于类的成员函数,有时候必须指定其为const类型,表明其是一个常函数,不能修改类的成员变量;
5)对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。

2、什么是死锁?

死锁就是线程1占有资源A去访问资源B,线程2占有资源B去访问资源A,这样就造成俩个线程谁也访问不到需要的资源

3、造成死锁的4个必要条件?

(1)互斥,同一时间统一资源只能由一个线程访问
(2)不可剥夺,当一个线程占有某资源时,只有该线程主动放弃该资源,外力无法解除
(3)请求和保持,线程1在占有某资源A的时候还可以请求资源B(吃着碗里看着锅里)
(4)回环,线程1占有资源A去请求资源B,线程2占有资源B去请求资源C,线程3占有资源C去请求资 源A

4、如何避免死锁?

(1)共有资源尽可能简短
(2)线程死锁等待超时则自动放弃请求并且释放自己占有的资源
(3)顺序加锁

5、static关键字作用?

(1)static修饰局部变量,局部变量的生命周期变长,函数执行结束不会立即释放内存
(2)static修饰全局变量,则该变量作用域变小,只能在当前文件使用,其它文件禁止用
(3)static修饰函数,函数作用域变小,只能在当前文件使用
(4)类的静态成员函数属于类,而不属于类的对象

6、c/c++中内存可以划分为几个部分?

(1)堆区 (例如malloc动态分配内存)
(2)栈区(局部变量)
(3)全局区(全局变量和静态变量)
(4)常量区
(5)程序代码区

7、new 和 malloc的区别?

(1)属性不一样,new是c++运算符,编译器支持就可以,而malloc是库函数,需要添加头文件才可以调用
(2)参数不一样,malloc分配内存的时候需要指定内存大小,而new根据类型自动计算所需空间大小
(3)返回值不一样,new返回的是对象类型的指针,而malloc返回的是void *的指针,需要强制类型转换
(4)new失败会抛出异常,malloc失败会返回NULL;
(5)new开辟的空间在自由存储区,malloc开辟的空间在堆区
(6)new的过程是先调用malloc开辟空间,之后在调用构造函数初始化成员变量。malloc没有初始化过程。delete是先调用析构函数,再调用free释放内存

8、计算机内部如何存储浮点数?

浮点数在存储中都分为三个部分:
(1)符号位(Sign) : 0代表正,1代表为负
(2)指数位(Exponent):用于存储科学计数法中的指数数据,并且采用移位存储(加127)
(3)尾数部分(Mantissa):尾数部分
C/C++面试笔试知识点总结_第1张图片
下面以float类型的数据8.25举例分析
8.25 = 1000.01 = 1.00001 x 2^3;
符号位(1bit):0
指数位置(8bit): 127 + 3
尾数(23bit):000 0100 0000 0000 0000 0000 0000

9、什么是虚函数?

虚函数是指一个类中你希望重写的成员函数,当你用一个基类指针或引用指向一个继承类对象的时候,你调用一个虚函数,实际调用的是继承类的版本,虚函数可以借助于指针或者引用来达到多态的效果。

10、什么是纯虚函数?

纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”,纯虚函数一定没有定义,纯虚函数用来规范派生类的行为,即接口。包含纯虚函数的类是抽象类。

参考链接:https://blog.csdn.net/Hackbuteer1/article/details/7558868

11、什么是抽象类?

包含纯虚函数的类是抽象类,抽象类不能定义实例。抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。抽象类不能定义实例,但可以声明指向实现该抽象类的具体类的指针或引用。

12、vector和list的区别?

(1)底层结构
  vector的底层结构是动态顺序表,在内存中是一段连续的空间。
  list的底层结构是带头节点的双向循环链表,在内存中不是一段连续的空间。
(2)访问
  vector支持随机访问,可以利用下标精准定位到一个元素上,访问某个元素的时间复杂度是O(1)。
  list不支持随机访问,要想访问list中的某个元素只能是从前向后或从后向前依次遍历,时间复杂度是O(N)。
(3)插入和删除
  vector任意位置插入和删除的效率低,因为它每插入一个元素(尾插除外),都需要搬移数据。
  list任意位置插入和删除的效率高,他不需要搬移元素,只需要改变插入或删除位置的前后两个节点的指向即可。

13、空类创建时会自带哪些函数?

(1)构造函数
(2)析构函数
(3)拷贝构造函数
(4)=运算符重载函数
(5)&运算符重载函数
(6)const修饰的&运算符重载函数

参考链接:https://blog.csdn.net/peiyao456/article/details/51834981

14、指针和引用的区别?

(1)指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来
的变量实质上是同一个东西,只不过是原变量的一个别名而已。如:

int a=1;int *p=&a; 
int a=1;iint &b=a;

(2)引用不可以为空,当被创建的时候,必须初始化,而指针可以是空值,可以在任何时候被初始化。
(3)指针可以有多级,但是引用只能是一级(int **p;合法 而 int &&a是不合法的)
(4)指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了。
(5)”sizeof引用” 得到的是所指向的变量(对象)的大小,而”sizeof指针”得到的是指针本身的大小;

参考链接:https://blog.csdn.net/qq_27678917/article/details/70224813

15、gcc程序编译过程?

(1)预处理,宏替换,头文件展开等操作
(2)编译,c文件编译成汇编文件
(3)汇编,汇编文件编译成二进制文件
(4)链接,链接库文件

16、什么是内存泄漏?采用哪些方法来避免和减少这类错误?

用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元即为内存泄露。
(1). 使用的时候要记得指针的长度
(2). malloc的时候得确定在哪里free
(3). 对指针赋值的时候应该注意被赋值指针需要不需要释放
(4). 动态分配内存的指针最好不要再次赋值
(5). 在C++中应该优先考虑使用智能指针
(6). 析构函数尽量使用虚函数

17、结构体对齐问题

对齐原则:结构体的长度.必须是其内部最大成员的整数倍.不足的要补齐.
下面为32位编译器程序运行结果:

#include 

struct A   //8
{
    int a;
    char b;
    short c;
};

struct B // 12
{
    char b;
    int a;
    short c;
};

struct C  //24
{
    double t;
    char b;
    int a;
    short c;
};

struct D   //24
{
    char a;
    double b;
    char c;
    char d;
};

int main(int argc, char *argv[])
{
    qDebug() << "sizeof(A) = " << sizeof(A);
    qDebug() << "sizeof(B) = " << sizeof(B);
    qDebug() << "sizeof(C) = " << sizeof(C);
    qDebug() << "sizeof(D) = " << sizeof(D);

    return 0;
}

/***************************************
运行结果:
sizeof(A) =  8
sizeof(B) =  12
sizeof(C) =  24
sizeof(D) =  24
***************************************/

这里需要注意结构体C,C结构体只是在B结构体前加了一个double,其它都一样,这里有递归的意味。

18、重载overload,覆盖override,隐藏overwrite,这三者之间的区别

(1)overload,重载:是指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。

class A{
public:
  void test(int i);
  void test(double i);//overload
  void test(int i, double j);//overload
  void test(double i, int j);//overload
  int test(int i);         //错误,非重载。注意重载不关心函数返回类型。
};

(2)重写(覆盖):是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有virtual修饰。

#include

using namespace std;

class Base
{
public:
    virtual void fun(int i){ cout << "Base::fun(int) : " << i << endl;}
};

class Derived : public Base
{
public:
    virtual void fun(int i){ cout << "Derived::fun(int) : " << i << endl;}
};
int main()
{
    Base b;
    Base * pb = new Derived();
    pb->fun(3);//Derived::fun(int)

    system("pause");
    return 0;
}

(3)隐藏:指派生类的函数屏蔽了与其同名的基类函数,注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。

#include "stdafx.h"
#include "iostream"

using namespace std;

class Base
{
public:
    void fun(double ,int ){ cout << "Base::fun(double ,int )" << endl; }
};

class Derive : public Base
{
public:
    void fun(int ){ cout << "Derive::fun(int )" << endl; }
};

int main()
{
    Derive pd;
    pd.fun(1);//Derive::fun(int )
    pb.fun(0.01, 1);//error C2660: “Derive::fun”: 函数不接受 2 个参数

    Base *fd = &pd;
    fd->fun(1.0,1);//Base::fun(double ,int);
    fd->fun(1);//error
    system("pause");
    return 0;
}

三者对比:
重载:相同范围(同一个类中)、 函数名字相同、参数不同、virtual关键字可有可无
覆盖:不同范围(基类和派生类)、函数名字相同、参数相同、基类中必须有virtual关键字(必须是虚函数)
隐藏:不同范围(基类和派生类)、函数名字相同、参数不同或者参数相同且无virtual关键字

参考链接:https://blog.csdn.net/zx3517288/article/details/48976097

19、c++代码如何调用c语言代码?

使用extern “C”,extern "C"主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。

20、什么是野指针?

野指针是未初始化或者未清零的指针,它指向的内存地址不是程序员所期望的,可能指向了受限的内存
成因:
1)指针变量没有被初始化
2)指针指向的内存被释放了,但是指针没有置NULL

21、栈溢出的原因以及解决方法?

1)函数调用层次过深,每调用一次,函数的参数、局部变量等信息就压一次栈,如递归次数太多
2)局部变量体积太大。
解决办法:
(1)增加栈内存;vs2019增加栈内存方法如下图
C/C++面试笔试知识点总结_第2张图片

(2)使用堆内存;具体实现由很多种方法可以直接把数组定义改成指针,然后动态申请内存;也可以把局部变量变成全局变量

22、宏和内联函数的区别?

(1)宏定义和内联函数使用的时候都是进行代码展开。不同的是宏定义是在预处理的时候把所有的宏名替换,内联函数则是在编译阶段把所有调用内联函数的地方把内联函数插入。这样可以省去函数压栈退栈,提高了效率
(2)宏定义不是函数。预处理器用复制宏代码的方式代替函数的调用,省去了函数压栈退栈过程,提高了效率。内联函数本质上是一个函数,内联函数一般用于函数体的代码比较简单的函数,不能包含复杂的控制语句while、switch,并且内联函数本身不能直接调用自身。如果内联函数的函数体过大,编译器会自动的把这个内联函数变成普通函数。
(3) 宏定义是在预编译的时候把所有的宏名用宏体来替换,简单的说就是字符串替换, 内联函数则是在编译的时候进行代码插入,编译器会在每处调用内联函数的地方直接把内联函数的内容展开,这样可以省去函数的调用的开销,提高效率
(4)宏定义是没有类型检查的,无论对还是错都是直接替换, 内联函数在编译的时候会进行类型的检查,内联函数满足函数的性质,比如有返回值、参数列表等

23、类的静态成员变量和静态成员函数各有哪些特性?

1、静态成员变量
1). 静态成员变量需要在类内声明(加static),在类外初始化(不能加static);
2). 静态成员变量在类外单独分配存储空间,位于全局数据区,因此静态成员变量的生命周期不依赖于类的某个对象,而是所有类的对象共享静态成员变量;
3). 可以通过对象名直接访问公有静态成员变量;
4). 可以通过类名直接调用公有静态成员变量,即不需要通过对象,这一点是普通成员变量所不具备的。

class App
{
public:
    static QString ConfigFile;          //配置文件文件路径及名称

    //全局通用变量
    static int MapWidth;                //地图宽度
    static int MapHeight;               //地图高度
    static bool IsMove;                 //设备是否可以移动
    static bool DbError;                //数据库是否错误
    static bool IsGpuDisplay;           //是否使用gpu来渲染视频
    static QString CurrentUrl;          //当前选中的视频,用于云台控制
    static QString CurrentImage;        //当前对应地图
    static QString CurrentUserName;     //当前用户名
    static QString CurrentUserPwd;      //当前用户密码
    static QString CurrentUserType;     //当前用户类型(值班员/管理员)    
    static QString FileFilter;          //文件拓展名过滤
    static QString FileExtension;       //导出文件拓展名
    static QString FileSpliter;         //导出文件内容分隔符
}

QString App::ConfigFile = "config.ini";
int App::MapWidth = 800;
int App::MapHeight = 600;
bool App::IsMove = false;
bool App::DbError = false;
bool App::IsGpuDisplay = false;
QString App::CurrentUrl = "";
QString App::CurrentImage = "bg_alarm.jpg";
QString App::CurrentUserName = "admin";
QString App::CurrentUserPwd = "admin";
QString App::CurrentUserType = QString::fromUtf8("管理员");
QString App::FileFilter = QString::fromUtf8("保存文件(*.csv)");
QString App::FileExtension = ".video";
QString App::FileSpliter = ",";

cout<<App::MapWidth; //可以直接通过类名调用静态成员变量

2、静态成员函数
1). 静态成员函数是类所共享的;
2). 静态成员函数可以访问静态成员变量,但是不能直接访问普通成员变量(需要通过对象来访问);需要注意的是普通成员函数既可以访问普通成员变量,也可以访问静态成员变量;
3). 可以通过对象名直接访问公有静态成员函数;
4). 可以通过类名直接调用公有静态成员函数,即不需要通过对象,这一点是普通成员函数所不具备的。

class App
{
    static void readConfig();           //读取配置文件
};
void App::readConfig()
{
    QSettings set(App::ConfigFile, QSettings::IniFormat);
    set.beginGroup("BaseConfig");
    App::StyleName = set.value("StyleName", App::StyleName).toString();
    App::Company = set.value("Company", App::Company).toString();
    App::LogoCn = set.value("LogoCn", App::LogoCn).toString();
    App::LogoEn = set.value("LogoEn", App::LogoEn).toString();
    App::LogoBg = set.value("LogoBg", App::LogoBg).toString();
    App::MsgCount = set.value("MsgCount", App::MsgCount).toInt();
    App::AutoRun = set.value("AutoRun", App::AutoRun).toBool();
    App::AutoLogin = set.value("AutoLogin", App::AutoLogin).toBool();
    App::AutoPwd = set.value("AutoPwd", App::AutoPwd).toBool();
    App::LastLoginer = set.value("LastLoginer", App::LastLoginer).toString();
    App::LastFormMain = set.value("LastFormMain", App::LastFormMain).toString();
    App::LastFormData = set.value("LastFormData", App::LastFormData).toString();
    App::LastFormConfig = set.value("LastFormConfig", App::LastFormConfig).toString();
    App::SelectDirName = set.value("SelectDirName", App::SelectDirName).toString();
    App::IsGpuDisplay = set.value("IsGpuDisplay", App::IsGpuDisplay).toBool();
    set.endGroup();
}
App::readConfig(); //可以直接通过类名调用静态成员变量

原文链接:https://blog.csdn.net/kuweicai/article/details/82779648

24、指针数组和数组指针的区别?

1、数组指针,是指向数组的指针,而指针数组则是指该数组的元素均为指针。
数组指针,是指向数组的指针,其本质为指针,形式如下。如 int (*p)[10],p即为指向数组的指针,()优先级高,首先说明p是一个指针,指向一个整型的一维数组,这个一维数组的长度是10,步长是一个int数据的长度。也就是说执行p+1时,p要跨过1个整型数据的长度。数组指针是指向数组首元素的地址的指针,其本质为指针,可以看成是二级指针。
类型名 (数组标识符)[数组长度]
2、指针数组,在C语言和C++中,数组元素全为指针的数组称为指针数组,其中一维指针数组的定义形式如下。指针数组中每一个元素均为指针,其本质为数组。如 int * p[n], []优先级高,先与p结合成为一个数组,再由int
说明这是一个整型指针数组,它有n个指针类型的数组元素。这里执行p+1时,则p指向下一个数组元素。
类型名 *数组标识符[数组长度]
原文链接:https://blog.csdn.net/kuweicai/article/details/82779648

25、虚函数表

多态是由虚函数实现的,而虚函数主要是通过虚函数表(V-Table)来实现的。
如果一个类中包含虚函数(virtual修饰的函数),那么这个类就会包含一张虚函数表,虚函数表存储的每一项是一个虚函数的地址。如下图:
C/C++面试笔试知识点总结_第3张图片
这个类的每一个对象都会包含一个虚指针(虚指针存在于对象实例地址的最前面,保证虚函数表有最高的性能),这个虚指针指向虚函数表。注:对象不包含虚函数表,只有虚指针,类才包含虚函数表,派生类会生成一个兼容基类的虚函数表。

(1)原始基类的虚函数表,下图是原始基类的对象,可以看到虚指针在地址的最前面,指向基类的虚函数表(假设基类定义了3个虚函数)
C/C++面试笔试知识点总结_第4张图片
(2)单继承时的虚函数(无重写基类虚函数),假设现在派生类继承基类,并且重新定义了3个虚函数,派生类会自己产生一个兼容基类虚函数表的属于自己的虚函数表。
C/C++面试笔试知识点总结_第5张图片
Derive class 继承了 Base class 中的三个虚函数,准确的说,是该函数实体的地址被拷贝到 Derive类的虚函数表,派生类新增的虚函数置于虚函数表的后面,并按声明顺序存放。

(3)单继承时的虚函数(重写基类虚函数),现在派生类重写基类的x函数,可以看到这个派生类构建自己的虚函数表的时候,修改了base::x()这一项,指向了自己的虚函数。
C/C++面试笔试知识点总结_第6张图片
原文链接:https://blog.csdn.net/qq_15079039/article/details/105927075

26、什么是多态?

什么是多态?

概念:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果,这就是多态性。简单的说:就是用基类的引用指向子类的对象。

为什么要用多态呢?

原因:我们知道,封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类);它们的目的都是为了——代码重用。而多态除了代码的复用性外,还可以解决项目中紧偶合的问题,提高程序的可扩展性.。耦合度讲的是模块模块之间,代码代码之间的关联度,通过对系统的分析把他分解成一个一个子模块,子模块提供稳定的接口,达到降低系统耦合度的的目的,模块模块之间尽量使用模块接口访问,而不是随意引用其他模块的成员变量。

多态有什么好处?

有两个好处:

  1. 应用程序不必为每一个派生类编写功能调用,只需要对抽象基类进行处理即可。大大提高程序的可复用性。//继承
  2. 派生类的功能可以被基类的方法或引用变量所调用,这叫向后兼容,可以提高可扩充性和可维护性。 //多态的真正作用,

原文链接:https://www.cnblogs.com/hai-ping/articles/2807750.html

27、C++的四种强制转换

在C++语言中新增了四个关键字static_cast、const_cast、reinterpret_cast和dynamic_cast。这四个关键字都是用于强制类型转换的。新类型的强制转换可以提供更好的控制强制转换过程,允许控制各种不同种类的强制转换。

原文链接:https://www.cnblogs.com/Allen-rg/p/6999360.html

28、sizeof 和 strlen 的区别?

(1) sizeof 是一个操作符,strlen 是库函数。
(2)sizeof 的参数可以是数据的类型,也可以是变量,而 strlen 只能以结尾为‘\0‘的字符串作参数。
(3) 编译器在编译时就计算出了 sizeof 的结果。而 strlen 函数必须在运行时才能计算出来。并且 sizeof 计算的是数据类型占内存的大小,而 strlen 计算的是字符串实际的长度。
(4) 数组做 sizeof 的参数不退化,传递给 strlen 就退化为指针了。
注意:有些是操作符看起来像是函数,而有些函数名看起来又像操作符,这类容易混淆的名称一定要加以区分,否则遇到数组名这类特殊数据类型作参数时就很容易出错。最容易混淆为函数的操作符就是 sizeof。

原文链接:https://blog.csdn.net/BostonRayAlen/article/details/93041395

29、单链表反转

    public static ListNode reverseListByInsert(ListNode listNode){
        //定义一个带头节点的
        ListNode resultList = new ListNode(-1);
        //循环节点
        ListNode p = listNode;
        while(p!= null){
            //保存插入点之后的数据
            ListNode tempList = p.next;
            p.next = resultList.next;
            resultList.next = p;
            p = tempList;
        }
        return resultList.next;
    }

30、进程和线程的区别?

线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process),它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。

根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位

资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的

影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行

原文链接:https://blog.csdn.net/ThinkWon/article/details/102021274

31、TCP/IP网络模型

TCP/IP 是互联网相关的各类协议族的总称,比如:TCP,UDP,IP,FTP,HTTP,ICMP,SMTP 等都属于 TCP/IP 族内的协议。
TCP/IP模型是互联网的基础,它是一系列网络协议的总称。这些协议可以划分为四层,分别为链路层、网络层、传输层和应用层。
C/C++面试笔试知识点总结_第7张图片

链路层:负责封装和解封装IP报文,发送和接受ARP/RARP报文等。
网络层:负责路由以及把分组报文发送给目标网络或主机。
传输层:负责对报文进行分组和重组,并以TCP或UDP协议格式封装报文。
应用层:负责向用户提供应用程序,比如HTTP、FTP、Telnet、DNS、SMTP等。

在网络体系结构中网络通信的建立必须是在通信双方的对等层进行,不能交错。 在整个数据传输过程中,数据在发送端时经过各层时都要附加上相应层的协议头和协议尾(仅数据链路层需要封装协议尾)部分,也就是要对数据进行协议封装,以标识对应层所用的通信协议。

32、tcp协议

当一台计算机想要与另一台计算机通讯时,两台计算机之间的通信需要畅通且可靠,这样才能保证正确收发数据。例如,当你想查看网页或查看电子邮件时,希望完整且按顺序查看网页,而不丢失任何内容。当你下载文件时,希望获得的是完整的文件,而不仅仅是文件的一部分,因为如果数据丢失或乱序,都不是你希望得到的结果,于是就用到了TCP。

TCP协议全称是传输控制协议是一种面向连接的、可靠的、基于字节流的传输层通信协议,由 IETF 的RFC 793定义。TCP 是面向连接的、可靠的流协议。流就是指不间断的数据结构,你可以把它想象成排水管中的水流。

1. TCP连接过程
如下图所示,可以看到建立一个TCP连接的过程为(三次握手的过程):
C/C++面试笔试知识点总结_第8张图片
第一次握手
客户端向服务端发送连接请求报文段。该报文段中包含自身的数据通讯初始序号。请求发送后,客户端便进入 SYN-SENT 状态。

第二次握手
服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,该应答中也会包含自身的数据通讯初始序号,发送完成后便进入 SYN-RECEIVED 状态。

第三次握手
当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。客户端发完这个报文段后便进入 ESTABLISHED 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时连接建立成功。

这里可能大家会有个疑惑:为什么 TCP 建立连接需要三次握手,而不是两次?这是因为这是为了防止出现失效的连接请求报文段被服务端接收的情况,从而产生错误。
C/C++面试笔试知识点总结_第9张图片
2. TCP断开连接
C/C++面试笔试知识点总结_第10张图片
TCP 是全双工的,在断开连接时两端都需要发送 FIN 和 ACK。

第一次握手

若客户端 A 认为数据发送完成,则它需要向服务端 B 发送连接释放请求。

第二次握手

B 收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明 A 到 B 的连接已经释放,不再接收 A 发的数据了。但是因为 TCP 连接是双向的,所以 B 仍旧可以发送数据给 A。

第三次握手

B 如果此时还有没发完的数据会继续发送,完毕后会向 A 发送连接释放请求,然后 B 便进入 LAST-ACK 状态。

第四次握手

A 收到释放请求后,向 B 发送确认应答,此时 A 进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有 B 的重发请求的话,就进入 CLOSED 状态。当 B 收到确认应答后,也便进入 CLOSED 状态。

3. TCP协议的特点
(1) 面向连接
面向连接,是指发送数据之前必须在两端建立连接。建立连接的方法是“三次握手”,这样能建立可靠的连接。建立连接,是为数据的可靠传输打下了基础。

(2) 仅支持单播传输
每条TCP传输连接只能有两个端点,只能进行点对点的数据传输,不支持多播和广播传输方式。

(3) 面向字节流
TCP不像UDP一样那样一个个报文独立地传输,而是在不保留报文边界的情况下以字节流方式进行传输。

(4)可靠传输
对于可靠传输,判断丢包,误码靠的是TCP的段编号以及确认号。TCP为了保证报文传输的可靠,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的字节发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据(假设丢失了)将会被重传。

(5) 提供拥塞控制
当网络出现拥塞的时候,TCP能够减小向网络注入数据的速率和数量,缓解拥塞

(6) TCP提供全双工通信
TCP允许通信双方的应用程序在任何时候都能发送数据,因为TCP连接的两端都设有缓存,用来临时存放双向通信的数据。当然,TCP可以立即发送一个数据段,也可以缓存一段时间以便一次发送更多的数据段(最大的数据段大小取决于MSS)

原文链接:https://www.cnblogs.com/fundebug/p/differences-of-tcp-and-udp.html

33、udp协议

UDP协议全称是用户数据报协议,在网络中它与TCP协议一样用于处理数据包,是一种无连接的协议。在OSI模型中,在第四层——传输层,处于IP协议的上一层。UDP有不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。

它有以下几个特点:
(1) 面向无连接
首先 UDP 是不需要和 TCP一样在发送数据前进行三次握手建立连接的,想发数据就可以开始发送了。并且也只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作。
具体来说就是:
在发送端,应用层将数据传递给传输层的 UDP 协议,UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议,然后就传递给网络层了
在接收端,网络层将数据传递给传输层,UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作
(2) 有单播,多播,广播的功能
UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。
(3) UDP是面向报文的
发送方的UDP对应用程序交下来的报文,在添加首部后就向下交付IP层。UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因此,应用程序必须选择合适大小的报文
(4) 不可靠性
首先不可靠性体现在无连接上,通信都不需要建立连接,想发就发,这样的情况肯定不可靠。
并且收到什么数据就传递什么数据,并且也不会备份数据,发送数据也不会关心对方是否已经正确接收到数据了。

再者网络环境时好时坏,但是 UDP 因为没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP。
C/C++面试笔试知识点总结_第11张图片
(5) 头部开销小,传输数据报文时是很高效的。
C/C++面试笔试知识点总结_第12张图片
UDP 头部包含了以下几个数据:

两个十六位的端口号,分别为源端口(可选字段)和目标端口
整个数据报文的长度
整个数据报文的检验和(IPv4 可选 字段),该字段用于发现头部信息和数据中的错误
因此 UDP 的头部开销小,只有八字节,相比 TCP 的至少二十字节要少得多,在传输数据报文时是很高效的

原文链接:https://www.cnblogs.com/fundebug/p/differences-of-tcp-and-udp.html

34、tcp协议udp协议区别?

C/C++面试笔试知识点总结_第13张图片
TCP向上层提供面向连接的可靠服务 ,UDP向上层提供无连接不可靠服务。虽然 UDP 并没有 TCP 传输来的准确,但是也能在很多实时性要求高的地方有所作为,对数据准确性要求高,速度可以相对较慢的,可以选用TCP。

原文链接:https://www.cnblogs.com/fundebug/p/differences-of-tcp-and-udp.html

35、进程间的通信方式

(1)管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
(2)命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
(3)消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
(4)共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
(5)信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
(6)套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
(7)信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

参考链接:https://blog.csdn.net/zhaohong_bo/article/details/89552188

36、技术面试基础知识总结(强烈推荐)

C/C++ 技术面试基础知识总结,包括语言、程序库、数据结构、算法、系统、网络、链接装载库等知识及面试经验、招聘、内推等信息。
C/C++面试笔试知识点总结_第14张图片

原文链接:https://github.com/huihut/interview

37、C++ 中 struct 和 class区别?

总的来说,struct 更适合看成是一个数据结构的实现体,class 更适合看成是一个对象的实现体。
区别:最本质的一个区别就是默认的访问控制
struct 作为数据结构的实现体,它默认的数据访问控制是 public 的,而 class 作为对象的实现体,它默认的成员变量访问控制是 private 的。

38、TCP 黏包问题

TCP 是一个基于字节流的传输服务(UDP 基于报文的),“流” 意味着 TCP 所传输的数据是没有边界的。所以可能会出现两个数据包黏在一起的情况。
解决方案:
(1)发送定长包。如果每个消息的大小都是一样的,那么在接收对等方只要累计接收数据,直到数据等于一个定长的数值就将它作为一个消息。
(2)包头加上包体长度。包头是定长的 4 个字节,说明了包体的长度。接收对等方先接收包头长度,依据包头长度来接收包体。
在数据包之间设置边界,如添加特殊符号 \r\n 标记。FTP 协议正是这么做的。但问题在于如果数据正文中也含有 \r\n,则会误判为消息的边界。

39、socket中TCP的三次握手?

我们知道tcp建立连接要进行“三次握手”,即交换三个分组。大致流程如下:

客户端向服务器发送一个SYN J
服务器向客户端响应一个SYN K,并对SYN J进行确认ACK J+1
客户端再想服务器发一个确认ACK K+1
只有就完了三次握手,但是这个三次握手发生在socket的那几个函数中呢?请看下图:
C/C++面试笔试知识点总结_第15张图片

从图中可以看出,当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态;客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。

参考链接:https://www.cnblogs.com/skynet/archive/2010/12/12/1903949.html

40、socket的基本操作

(1)流程图
C/C++面试笔试知识点总结_第16张图片
(2)示例代码:server.c

#include   //printf
#include   //inet_addr htons
#include 
#include   //socket bind listen accept connect
#include   //sockaddr_in
#include  //exit
#include  //close
#include  //strcat

#define N 128
#define errlog(errmsg) do{\
					     	perror(errmsg);\
							printf("%s -- %s -- %d\n", __FILE__, __func__, __LINE__);\
							exit(1);\
					     }while(0);

int main(int argc, const char *argv[])
{
	int sockfd, acceptfd;
	struct sockaddr_in serveraddr, clientaddr;
	socklen_t addrlen = sizeof(serveraddr);
	char buf[N] = {};
	ssize_t bytes;

	if(argc < 3)
	{
		fprintf(stderr, "Error: usage %s  \n", argv[0]);
		exit(1);
	}

	//第一步:创建套接字
	if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
	{
		errlog("fail to socket");
	}

	//第二步:填充服务器网络信息结构体
	//inet_addr:将点分十进制ip地址转化为网络字节序的整型数据
	//htons:将主机字节序转化为网络字节序
	//atoi:将数字型字符串转化为整型数据
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
	serveraddr.sin_port = htons(atoi(argv[2]));

	//第三步:将套接字与网络信息结构体绑定
	if(bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
	{
		errlog("fail to bind");
	}

	//第四步:将套接字设置为监听模式
	if(listen(sockfd, 5) < 0)
	{
		errlog("fail to listen");
	}

	//第五步:阻塞等待客户端的连接请求
	if((acceptfd = accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen)) < 0)
	//if((acceptfd = accept(sockfd, NULL, NULL)) < 0)
	{
		errlog("fail to accept");
	}

	//打印一下客户端的信息
	printf("%s --> %d\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));

	while(1)
	{
		if((bytes = recv(acceptfd, buf, N, 0)) < 0)
		{
			errlog("fail to recv");
		}
		else if(bytes == 0)
		{
			printf("NO DATA\n");
			exit(1);
		}
		else
		{
			if(strncmp(buf, "quit", 4) == 0)
			{
				printf("client quited ...\n");
				break;
			}
			else
			{
				printf("client: %s\n", buf);

				strcat(buf, " *_*");

				if(send(acceptfd, buf, N, 0) < 0)
				{
					errlog("fail to send");
				}
			}
		}
	}

	close(acceptfd);
	close(sockfd);
	
	return 0;
}

client.c

#include   //printf
#include   //inet_addr htons
#include 
#include   //socket bind listen accept connect
#include   //sockaddr_in
#include  //exit
#include  //close
#include  //strcat

#define N 128
#define errlog(errmsg) do{\
					     	perror(errmsg);\
							printf("%s -- %s -- %d\n", __FILE__, __func__, __LINE__);\
							exit(1);\
					     }while(0);

int main(int argc, const char *argv[])
{
	int sockfd;
	struct sockaddr_in serveraddr;
	socklen_t addrlen = sizeof(serveraddr);
	char buf[N] = {};

	if(argc < 3)
	{
		fprintf(stderr, "Error: usage %s  \n", argv[0]);
		exit(1);
	}

	//第一步:创建套接字
	if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
	{
		errlog("fail to socket");
	}

	//第二步:填充服务器网络信息结构体
	//inet_addr:将点分十进制ip地址转化为网络字节序的整型数据
	//htons:将主机字节序转化为网络字节序
	//atoi:将数字型字符串转化为整型数据
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
	serveraddr.sin_port = htons(atoi(argv[2]));

#if 0
	//客户端也已自己指定自己的信息
	struct sockaddr_in clientaddr;
	clientaddr.sin_family = AF_INET;
	clientaddr.sin_addr.s_addr = inet_addr(argv[3]);
	clientaddr.sin_port = htons(atoi(argv[4]));

	if(bind(sockfd, (struct sockaddr *)&clientaddr, addrlen) < 0)
	{
		errlog("fail to bind");
	}
#endif

	//第三步:发送客户端的连接请求
	if(connect(sockfd, (struct sockaddr *)&serveraddr, addrlen) < 0)
	{
		errlog("fail to connect");
	}

	while(1)
	{
		fgets(buf, N, stdin);
		buf[strlen(buf) - 1] = '\0';

		if(send(sockfd, buf, N, 0) < 0)
		{
			errlog("fail to send");
		}

		if(strncmp(buf, "quit", 4) == 0)
		{
			break;
		}
		else
		{
			if(recv(sockfd, buf, N, 0) < 0)
			{
				errlog("fail to recv");
			}

			printf("server: %s\n", buf);
		}
	}

	close(sockfd);
	
	return 0;
}

41、子类析构时要调用父类的析构函数吗?

析构函数调用的次序是先派生类的析构后基类的析构,也就是说在基类的的析构调用的时候,派生类的信息已经全部销毁了。定义一个对象时先调用基类的构造函数、然后调用派生类的构造函数;析构的时候恰好相反:先调用派生类的析构函数、然后调用基类的析构函数。

42、int id[sizeof(unsigned long)];这个对吗?为什么?

答案:正确 这个 sizeof是编译时运算符,编译时就确定了 ,可以看成和机器有关的常量。

43、typedef 和 define 有什么区别

(1) 用法不同:typedef 用来定义一种数据类型的别名,增强程序的可读性。define 主要用来定义常量,以及书写复杂使用频繁的宏。
(2) 执行时间不同:typedef 是编译过程的一部分,有类型检查的功能。define 是宏定义,是预编译的部分,其发生在编译之前,只是简单的进行字符串的替换,不进行类型的检查。
(3) 作用域不同:typedef 有作用域限定。define 不受作用域约束,只要是在 define 声明后的引用都是正确的。
(4) 对指针的操作不同:typedef 和 define 定义的指针时有很大的区别。
注意:typedef 定义是语句,因为句尾要加上分号。而 define 不是语句,千万不能在句尾加分号。
原文链接:https://blog.csdn.net/bostonrayalen/article/details/93041395

44、为什么free()释放内存的时候不需要指定内存的大小?

申请内存块的时候,会有一种机制去记录该内存块的大小,各编译器做法可能不同,比如申请的时候多分配4个字节,用来存储整个内存块的大小。所以在free的时候只需要传指针就行了,不需要传大小,它自己知道的。

45、STL常用标准库容器

原文链接:https://blog.csdn.net/yuleidnf/article/details/81541736

46、信号与槽的优缺点?

(1) 降低程序的耦合度
(2) 子类可以调用父类函数
信号和槽机制是Qt的一大特征,信号和槽用于对象间的通讯。简单地说就是A函数发出一个Signal,此时B函数作为这个Signal的Slot被调用。
一直很不理解,A为什么不直接调用B,那样企不是更简单明了,中间用到Signal and Slot机制不是多此一举。事实的确如此,这种机制需要发射信号、定位连接对像,比直接调用函数慢地多。
但是存在总是有理由的,
在网上搜了Qt的优势:
大多都是说“与回调相比,它具有2个优点:1.类型安全;2.信号与槽的连接是松散的。”
听不懂他们在讲什么,就像专家一样,讲出来大家不懂的话才算有水平。

我水平有限,就只能用通俗的话来表达了,Qt的优势在什么地方?
自从程序设计使用分层的思想以来,特别是在一些大的工程中,高质量的代码会表现地等级森严(层次间的关连性很小),在样在代码的移植、管理和维护中有很大的好处。
在人类社会一样中,分派任务时,级别相同的好说话,上级分派给下级更好说话,但下级分给上级任务可想而知会有什么后果,我们的统治阶层是不允许这种事的发生。分层思想中,函数调用也是如此,上层可以调用下层和同一层的函数,下层函数不可以调用上层函数,否则程序的层次性会被打破,导致结构错综复杂,难以维护和管理。
下层有事情需要禀报的时候怎么办?上层会设立一个机构,也就是一个函数,用无限循环来查询下层的状态,如果下层真的有事情,这个机构就把这消息拿到上一层来处理。这种处理方式显得有些复杂,我们想要的简单明了的方式是,如果下层有事件发生,可以直接“调用”上层函数处理。
说了这么多其实就是想说,信号和槽的最大优势在于,它完善了程序分层的思想,它可以在不改变程序的层次性的情况下,完成由下层到上层的“调用”。在下层发出一个Signal,这时上层与其想关联的Slot函数就会响应。

信号与槽的缺点:缺点的话就是代码追踪起来比较费劲,举个例子,你正在浏览代码,突然蹦出来一句 emit signalA();完了你就不知道这个信号A发送完毕后到底做了什么,你是无法通过点击信号函数直接跳转的,前面说过,一个信号可能绑定多个槽函数,所以,这里追踪起来就不如直接调用函数那么简单,我们可能需要全局搜索这个信号,看看哪些模块绑定了这个信号,再一个一个去分析。

总结:相比于信号与槽函数的优点来说,缺点还是可以忍受的吧
原文链接:https://blog.csdn.net/q_q_zh/article/details/7906983

47、说说C++智能指针?

原文链接:https://blog.csdn.net/flowing_wind/article/details/81301001

48、http和https有什么区别?

1、HTTPS 协议需要到 CA (Certificate Authority,证书颁发机构)申请证书,一般免费证书较少,因而需要一定费用。(以前的网易官网是http,而网易邮箱是 https 。)

2、HTTP 是超文本传输协议,信息是明文传输,HTTPS 则是具有安全性的 SSL 加密传输协议。

3、HTTP 和 HTTPS 使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。

4、HTTP 的连接很简单,是无状态的。HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 HTTP 协议安全。(无状态的意思是其数据包的发送、传输和接收都是相互独立的。无连接的意思是指通信双方都不长久的维持对方的任何信息。)

原文链接:https://blog.csdn.net/qq_38289815/article/details/80969419

48、单核cpu多线程有必要吗?

现代计算机一般都是多核cpu,多线程的可以大大提高效率,但是可能会有疑问,那单核CPU使用多线程是不是没有必要了,假定一种情况,web应用服务器,单核CPU、单线程,用户发过来请求,单个线程处理,CPU等待这个线程的处理结果返回,查询数据库,CPU等待查询结果…,只有一个线程的话,每次线程在处理的过程中CPU都有大量的空闲等待时间,那这样来说并行和串行似乎并没有体现并行的优势,因为任务的总量在那里,实际情况肯定不是这样的,即便是单核CPU,一个进程中往往也是有多个线程存在的,每个线程各司其职,CPU来调度各线程。

这里需要区分CPU处理指令和IO读取的不同,CPU的执行速度要远大于IO的过程,因此在大多数情况下多一些复杂的CPU计算都比增加一次IO要快,这一块深入理解要学习计算机原理相关的知识。

原文链接:https://blog.csdn.net/luzhensmart/article/details/105892068

49、关于*P++的问题

#include 

using namespace std;

int main()
{
	// 1、(*p)++
	int a = 10;
	int* p = &a;
	int b = (*p)++; //b = *p  a = a + 1;
	cout << "a = " << a << endl; //a = 11
	cout << "b = " << b << endl; //b = 10;

	// 2、 ++(*p)
	int c = ++(*p); //c = 11+1  a = 12;
	cout << "a = " << a << endl; //a = 12
	cout << "c = " << c << endl; //c = 12;

	// 3、 *p++
	int arr[] = { 1,2,3,4,5 };
	int* p1 = arr;
	cout << "*p1++ = " << *p1++ << endl; //*p1++ = 1;

	// 4、 *++p
	cout << "*++p1 = " << *++p1 << endl; //*++p1 = 3;

	return 0;
}

你可能感兴趣的:(C语言,面试,c++,c语言)