C++面经
作者:互联网
C++基础
1. C/C++内存有哪几种类型?
C中,内存分为5个区:堆(malloc)、栈(如局部变量、函数参数)、程序代码区(存放二进制代码)、全局/静态存储区(全局变量、static变量)和常量存储区(常量)。此外,C++中有自由存储区(new)一说。
全局变量、static变量会初始化为零,而堆和栈上的变量是随机的,不确定的。
2. 堆和栈的区别?
- 1).堆存放动态分配的对象——即那些在程序运行时分配的对象,比如局部变量,其生存期由程序控制;
- 2).栈用来保存定义在函数内的非static对象,仅在其定义的程序块运行时才存在;
- 3).静态内存用来保存static对象,类static数据成员以及定义在任何函数外部的变量,static对象在使用之前分配,程序结束时销毁;
- 4).栈和静态内存的对象由编译器自动创建和销毁。
3. 堆和自由存储区的区别?
总的来说,堆是C语言和操作系统的术语,是操作系统维护的一块动态分配内存;自由存储是C++中通过new与delete动态分配和释放对象的抽象概念。他们并不是完全一样。
从技术上来说,堆(heap)是C语言和操作系统的术语。堆是操作系统所维护的一块特殊内存,它提供了动态分配的功能,当运行程序调用malloc()时就会从中分配,稍后调用free可把内存交还。而自由存储是C++中通过new和delete动态分配和释放对象的抽象概念,通过new来申请的内存区域可称为自由存储区。基本上,所有的C++编译器默认使用堆来实现自由存储,也即是缺省的全局运算符new和delete也许会按照malloc和free的方式来被实现,这时藉由new运算符分配的对象,说它在堆上也对,说它在自由存储区上也正确。
4. 程序编译的过程?
程序编译的过程中就是将用户的文本形式的源代码(c/c++)转化成计算机可以直接执行的机器代码的过程。主要经过四个过程:预处理、编译、汇编和链接。具体示例如下。
一个hello.c的c语言程序如下。
其编译过程如下:
5. 计算机内部如何存储负数和浮点数?
负数比较容易,就是通过一个标志位和补码来表示。
对于浮点类型的数据采用单精度类型(float)和双精度类型(double)来存储,float数据占用32bit,double数据占用64bit,我们在声明一个变量float f= 2.25f的时候,是如何分配内存的呢?如果胡乱分配,那世界岂不是乱套了么,其实不论是float还是double在存储方式上都是遵从IEEE的规范的,float遵从的是IEEE R32.24 ,而double 遵从的是R64.53。更多可以参考浮点数表示。
无论是单精度还是双精度在存储中都分为三个部分:
- 1). 符号位(Sign) : 0代表正,1代表为负
- 2). 指数位(Exponent):用于存储科学计数法中的指数数据,并且采用移位存储
- 3). 尾数部分(Mantissa):尾数部分
其中float的存储方式如下图所示:
而双精度的存储方式如下图:
6. 函数调用的过程?
如下结构的代码,
调用fun()的过程大致如下:
- main()========
- 1).参数拷贝(压栈),注意顺序是从右到左,即c-b-a;
- 2).保存d = fun(a, b, c)的下一条指令,即cout<<d<<endl(实际上是这条语句对应的汇编指令的起始位置);
- 3).跳转到fun()函数,注意,到目前为止,这些都是在main()中进行的;
- fun()=====
- 4).移动ebp、esp形成新的栈帧结构;
- 5).压栈(push)形成临时变量并执行相关操作;
- 6).return一个值;
- 7).出栈(pop);
- 8).恢复main函数的栈帧结构;
- 9).返回main函数;
- main()========
- 。。。
7. 左值和右值
不是很严谨的来说,左值指的是既能够出现在等号左边也能出现在等号右边的变量(或表达式),右值指的则是只能出现在等号右边的变量(或表达式)。举例来说我们定义的变量 a 就是一个左值,而malloc返回的就是一个右值。或者左值就是在程序中能够寻值的东西,右值就是一个具体的真实的值或者对象,没法取到它的地址的东西(不完全准确),因此没法对右值进行赋值,但是右值并非是不可修改的,比如自己定义的class, 可以通过它的成员函数来修改右值。
8. 什么是内存泄漏?面对内存泄漏和指针越界,你有哪些方法?你通常采用哪些方法来避免和减少这类错误?
用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元即为内存泄露。
- 1). 使用的时候要记得指针的长度.
- 2). malloc的时候得确定在那里free.
- 3). 对指针赋值的时候应该注意被赋值指针需要不需要释放.
- 4). 动态分配内存的指针最好不要再次赋值.
- 5). 在C++中应该优先考虑使用智能指针.
- 1). C++是C的超集;
- 2). C是一个结构化语言,它的重点在于算法和数据结构。C程序的设计首要考虑的是如何通过一个过程,对输入(或环境条件)进行运算处理得到输出(或实现过程(事务)控制),而对于C++,首要考虑的是如何构造一个对象模型,让这个模型能够契合与之对应的问题域,这样就可以通过获取对象的状态信息得到输出或实现过程(事务)控制。
第二部分:C v.s. C++
1. C和C++的区别?
2. int fun() 和 int fun(void)的区别?
这里考察的是c 中的默认类型机制。
- 在c中,int fun() 会解读为返回值为int(即使前面没有int,也是如此,但是在c++中如果没有返回类型将报错),输入类型和个数没有限制, 而int fun(void)则限制输入类型为一个void。
- 在c++下,这两种情况都会解读为返回int类型,输入void类型。
3. const 有什么用途
主要有三点:
- 1).定义只读变量,或者常量(只读变量和常量的区别参考下面一条);
- 2).修饰函数的参数和函数的返回值;
- 3).修饰函数的定义体,这里的函数为类的成员函数,被const修饰的成员函数代表不能修改成员变量的值,因此const成员函数只能调用const成员函数;
- 4).只读对象。只读对象只能调用const成员函数。
const Screen screen; //只读对象
- 1
- 2
- 3
- 4
- 5
- 6
- 7
4. 在C中用const 能定义真正意义上的常量吗?C++中的const呢?
不能。c中的const仅仅是从编译层来限定,不允许对const 变量进行赋值操作,在运行期是无效的,所以并非是真正的常量(比如通过指针对const变量是可以修改值的),但是c++中是有区别的,c++在编译时会把const常量加入符号表,以后(仍然在编译期)遇到这个变量会从符号表中查找,所以在C++中是不可能修改到const变量的。
补充:
- 1). c中的局部const常量存储在栈空间,全局const常量存在只读存储区,所以全局const常量也是无法修改的,它是一个只读变量。
- 2). 这里需要说明的是,常量并非仅仅是不可修改,而是相对于变量,它的值在编译期已经决定,而不是在运行时决定。
- 3).c++中的const 和宏定义是有区别的,宏是在预编译期直接进行文本替换,而const发生在编译期,是可以进行类型检查和作用域检查的。
- 4).c语言中只有enum可以实现真正的常量。
- 5). c++中只有用字面量初始化的const常量会被加入符号表,而变量初始化的const常量依然只是只读变量。
- 6). c++中const成员为只读变量,可以通过指针修改const成员的值,另外const成员变量只能在初始化列表中进行初始化。
下面我们通过代码来看看区别。
同样一段代码,在c编译器下,打印结果为*pa = 4, 4
在c++编译下打印的结果为 *pa = 4, 8
另外值得一说的是,由于c++中const常量的值在编译期就已经决定,下面的做法是OK的,但是c中是编译通不过的。
5. 宏和内联(inline)函数的比较?
- 1). 首先宏是C中引入的一种预处理功能;
- 2). 内联(inline)函数是C++中引用的一个新的关键字;C++中推荐使用内联函数来替代宏代码片段;
- 3). 内联函数将函数体直接扩展到调用内联函数的地方,这样减少了参数压栈,跳转,返回等过程;
- 4). 由于内联发生在编译阶段,所以内联相较宏,是有参数检查和返回值检查的,因此使用起来更为安全;
- 5). 需要注意的是, inline会向编译期提出内联请求,但是是否内联由编译期决定(当然可以通过设置编译器,强制使用内联);
- 6). 由于内联是一种优化方式,在某些情况下,即使没有显示的声明内联,比如定义在class内部的方法,编译器也可能将其作为内联函数。
- 7). 内联函数不能过于复杂,最初C++限定不能有任何形式的循环,不能有过多的条件判断,不能对函数进行取地址操作等,但是现在的编译器几乎没有什么限制,基本都可以实现内联。
更多请参考inline关键字 - 1). malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
- 2). 对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。
由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。
最后补充一点体外话,new 在申请内存的时候就可以初始化(如下代码), 而malloc是不允许的。另外,由于malloc是库函数,需要相应的库支持,因此某些简易的平台可能不支持,但是new就没有这个问题了,因为new是C++语言所自带的运算符。
6. C++中有了malloc / free , 为什么还需要 new / delete?
特别的,在C++中,如下的代码,用new创建一个对象(new 会触发构造函数, delete会触发析构函数),但是malloc仅仅申请了一个空间,所以在C++中引入new和delete来支持面向对象。
Test* pn = new Test;
Test* pm = (Test*)malloc(sizeof(Test));
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
7. C和C++中的强制类型转换?
C中是直接在变量或者表达式前面加上(小括号括起来的)目标类型来进行转换,一招走天下,操作简单,但是由于太过直接,缺少检查,因此容易发生编译检查不到错误,而人工检查又及其难以发现的情况;而C++中引入了下面四种转换:
- 1). static_cast
a. 用于基本类型间的转换
b. 不能用于基本类型指针间的转换
c. 用于有继承关系类对象间的转换和类指针间的转换 - 2). dynamic_cast
a. 用于有继承关系的类指针间的转换
b. 用于有交叉关系的类指针间的转换
c. 具有类型检查的功能
d. 需要虚函数的支持 - 3). reinterpret_cast
a. 用于指针间的类型转换
b. 用于整数和指针间的类型转换 - 4). const_cast
a. 用于去掉变量的const属性
b. 转换的目标类型必须是指针或者引用
在C++中,普通类型可以通过类型转换构造函数转换为类类型,那么类可以转换为普通类型吗?答案是肯定的。但是在工程应用中一般不用类型转换函数,因为无法抑制隐式的调用类型转换函数(类型转换构造函数可以通过explicit来抑制其被隐式的调用),而隐式调用经常是bug的来源。实际工程中替代的方式是定义一个普通函数,通过显式的调用来达到类型转换的目的。
};
int main()
{
…
test a(5);
int i = a;
…
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 1). 静态(局部/全局)变量
- 2). 静态函数
- 3). 类的静态数据成员
- 4). 类的静态成员函数
- 1). 静态成员变量需要在类内声明(加static),在类外初始化(不能加static),如下例所示;
- 2). 静态成员变量在类外单独分配存储空间,位于全局数据区,因此静态成员变量的生命周期不依赖于类的某个对象,而是所有类的对象共享静态成员变量;
- 3). 可以通过对象名直接访问公有静态成员变量;
- 4). 可以通过类名直接调用公有静态成员变量,即不需要通过对象,这一点是普通成员变量所不具备的。
8. static 有什么用途
9. 类的静态成员变量和静态成员函数各有哪些特性?
静态成员变量
int example::m_int = 0; //没有static
cout<<example::m_int; //可以直接通过类名调用静态成员变量
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1). 静态成员函数是类所共享的;
- 2). 静态成员函数可以访问静态成员变量,但是不能直接访问普通成员变量(需要通过对象来访问);需要注意的是普通成员函数既可以访问普通成员变量,也可以访问静态成员变量;
- 3). 可以通过对象名直接访问公有静态成员函数;
- 4). 可以通过类名直接调用公有静态成员函数,即不需要通过对象,这一点是普通成员函数所不具备的。
静态成员函数
cout<<example::getI(); //可以直接通过类名调用静态成员变量
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
10. 在C++程序中调用被C编译器编译后的函数,为什么要加extern“C”?
C++语言支持函数重载,C语言不支持函数重载,函数被C++编译器编译后在库中的名字与C语言的不同,假设某个函数原型为:
该函数被C编译器编译后在库中的名字为 _foo, 而C++编译器则会产生像: _foo_int_int 之类的名字。为了解决此类名字匹配的问题,C++提供了C链接交换指定符号 extern “C”。
11. 头文件中的 ifndef/define/endif 是干什么用的? 该用法和 program once 的区别?
相同点:
它们的作用是防止头文件被重复包含。
不同点
- 1). ifndef 由语言本身提供支持,但是 program once 一般由编译器提供支持,也就是说,有可能出现编译器不支持的情况(主要是比较老的编译器)。
- 2). 通常运行速度上 ifndef 一般慢于 program once,特别是在大型项目上, 区别会比较明显,所以越来越多的编译器开始支持 program once。
- 3). ifndef 作用于某一段被包含(define 和 endif 之间)的代码, 而 program once 则是针对包含该语句的文件, 这也是为什么 program once 速度更快的原因。
- 4). 如果用 ifndef 包含某一段宏定义,当这个宏名字出现“撞车”时,可能会出现这个宏在程序中提示宏未定义的情况(在编写大型程序时特性需要注意,因为有很多程序员在同时写代码)。相反由于program once 针对整个文件, 因此它不存在宏名字“撞车”的情况, 但是如果某个头文件被多次拷贝,program once 无法保证不被多次包含,因为program once 是从物理上判断是不是同一个头文件,而不是从内容上。
12. 当i是一个整数的时候++i和i++那个更快一点?i++和++i的区别是什么?
答:理论上++i更快,实际与编译器优化有关,通常几乎无差别。
// ++i实现代码为:
int& operator++()
{
*this += 1;
return *this;
}//返回一个int型的对象引用
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
i++和++i的考点比较多,简单来说,就是i++返回的是i的值,而++i返回的是i+1的值。也就是++i是一个确定的值,是一个可修改的左值,如下使用:
可以不停的嵌套++i。
这里有很多的经典笔试题,一起来观摩下:
首先是函数的参数入栈顺序从右向左入栈的,计算顺序也是从右往左计算的,不过都是计算完以后再进行的压栈操作:
对于第1个printf,首先执行++i,返回值是i,这时i的值是2,再次执行++i,返回值是i,得到i=3,将i压入栈中,此时i为3,也就是压入3,3;
对于第2个printf,首先执行i++,返回值是原来的i,也就是3,再执行++i,返回值是i,依次将3,5压入栈中得到输出结果
对于第3个printf,首先执行i++,返回值是5,再执行i++返回值是6,依次将5,6压入栈中得到输出结果
对于第4个printf,首先执行++i,返回i,此时i为8,再执行i++,返回值是8,此时i为9,依次将i,8也就是9,8压入栈中,得到输出结果。
上面的分析也是基于VS搞的,不过准确来说函数多个参数的计算顺序是未定义的(the order of evaluation of function arguments are undefined)。笔试题目的运行结果随不同的编译器而异。
第三部分:数组、指针 & 引用
1. 指针和引用的区别?
相同点:
- 1). 都是地址的概念;
- 2). 都是“指向”一块内存。指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名;
- 3). 引用在内部实现其实是借助指针来实现的,一些场合下引用可以替代指针,比如作为函数形参。
不同点:
- 1). 指针是一个实体,而引用(看起来,这点很重要)仅是个别名;
- 2). 引用只能在定义时被初始化一次,之后不可变;指针可变;引用“从一而终”,指针可以“见异思迁”;
- 3). 引用不能为空,指针可以为空;
- 4). “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小;
- 5). 指针和引用的自增(++)运算意义不一样;
- 6). 引用是类型安全的,而指针不是 (引用比指针多了类型检查)
- 7). 引用具有更好的可读性和实用性。
2. 引用占用内存空间吗?
如下代码中对引用取地址,其实是取的引用所对应的内存空间的地址。这个现象让人觉得引用好像并非一个实体。但是引用是占用内存空间的,而且其占用的内存和指针一样,因为引用的内部实现就是通过指针来完成的。
比如 Type& name; <===> Type* const name。
3. 三目运算符
在C中三目运算符(? :)的结果仅仅可以作为右值,比如如下的做法在C编译器下是会报错的,但是C++中却是可以是通过的。这个进步就是通过引用来实现的,因为下面的三目运算符的返回结果是一个引用,然后对引用进行赋值是允许的。
4. 指针数组和数组指针的区别
数组指针,是指向数组的指针,而指针数组则是指该数组的元素均为指针。
- 数组指针,是指向数组的指针,其本质为指针,形式如下。如 int (*p)[10],p即为指向数组的指针,()优先级高,首先说明p是一个指针,指向一个整型的一维数组,这个一维数组的长度是n,也可以说是p的步长。也就是说执行p+1时,p要跨过n个整型数据的长度。数组指针是指向数组首元素的地址的指针,其本质为指针,可以看成是二级指针。
- 指针数组,在C语言和C++中,数组元素全为指针的数组称为指针数组,其中一维指针数组的定义形式如下。指针数组中每一个元素均为指针,其本质为数组。如 int *p[n], []优先级高,先与p结合成为一个数组,再由int*说明这是一个整型指针数组,它有n个指针类型的数组元素。这里执行p+1时,则p指向下一个数组元素,这样赋值是错误的:p=a;因为p是个不可知的表示,只存在p[0]、p[1]、p[2]…p[n-1],而且它们分别是指针变量可以用来存放变量地址。但可以这样 *p=a; 这里*p表示指针数组第一个元素的值,a的首地址的值。
第四部分:C++特性
1. 什么是面向对象(OOP)?面向对象的意义?
Object Oriented Programming, 面向对象是一种对现实世界理解和抽象的方法、思想,通过将需求要素转化为对象进行问题处理的一种思想。其核心思想是数据抽象、继承和动态绑定(多态)。
面向对象的意义在于:将日常生活中习惯的思维方式引入程序设计中;将需求中的概念直观的映射到解决方案中;以模块为中心构建可复用的软件系统;提高软件产品的可维护性和可扩展性。
2. 解释下封装、继承和多态?
- 1). 封装:
封装是实现面向对象程序设计的第一步,封装就是将数据或函数等集合在一个个的单元中(我们称之为类)。
封装的意义在于保护或者防止代码(数据)被我们无意中破坏。 - 2). 继承:
继承主要实现重用代码,节省开发时间。
子类可以继承父类的一些东西。
a. 公有继承(public)
公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员仍然是私有的,不能被这个派生类的子类所访问。
b. 私有继承(private)
私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问。
c. 保护继承(protected)
保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有的。 - 1). 没有任何构造函数时,编译器会自动生成默认构造函数,也就是无参构造函数;当类没有拷贝构造函数时,会生成默认拷贝构造函数。
- 2). 深拷贝是指拷贝后对象的逻辑状态相同,而浅拷贝是指拷贝后对象的物理状态相同;默认拷贝构造函数属于浅拷贝。
- 3). 当系统中有成员指代了系统中的资源时,需要深拷贝。比如指向了动态内存空间,打开了外存中的文件或者使用了系统中的网络接口等。如果不进行深拷贝,比如动态内存空间,可能会出现多次被释放的问题。是否需要定义拷贝构造函数的原则是,是类是否有成员调用了系统资源,如果定义拷贝构造函数,一定是定义深拷贝,否则没有意义。
- 1). 首先调用父类的构造函数;
- 2). 调用成员变量的构造函数;
- 3). 调用类自身的构造函数。
3. 什么时候生成默认构造函数(无参构造函数)?什么时候生成默认拷贝构造函数?什么是深拷贝?什么是浅拷贝?默认拷贝构造函数是哪种拷贝?什么时候用深拷贝?
4. 构造函数和析构函数的执行顺序?
构造函数
析构函数
对于栈对象或者全局对象,调用顺序与构造函数的调用顺序刚好相反,也即后构造的先析构。对于堆对象,析构顺序与delete的顺序相关。
5. C++的编译环境
如下图所示,C++的编译环境由如下几部分构成:C++标准库、C语言兼容库、编译器扩展库及编译模块。
值得注意的是,C语言兼容库功能上跟C++标准库中的C语言子库相同,它的存中主要为了兼容C语言编译器,也就是说如果一个文件只包含C语言兼容库(不包含C++标准库),那么它在C语言编译器中依然可以编译通过。
QT
一、讲述Qt信号槽机制与优势与不足
优点: ①类型安全。需要关联的信号槽的签名必须是等同的。即信号的参数类型和参数个数同接受该信号的槽的参数类型和参数个数相同。若信号和槽签名不一致,编译器会报错。
②松散耦合。信号和槽机制减弱了Qt对象的耦合度。激发信号的Qt对象无需知道是那个对象的那个信号槽接收它发出的信号,它只需在适当的时间发送适当的信号即可,而不需要关心是否被接受和那个对象接受了。Qt就保证了适当的槽得到了调用,即使关联的对象在运行时被删除。程序也不会奔溃。
③灵活性。一个信号可以关联多个槽,或多个信号关联同一个槽。
不足:速度较慢。与回调函数相比,信号和槽机制运行速度比直接调用非虚函数慢10倍。
原因:①需要定位接收信号的对象。②安全地遍历所有关联槽。③编组、解组传递参数。④多线程的时候,信号需要排队等待。(然而,与创建对象的new操作及删除对象的delete操作相比,信号和槽的运行代价只是他们很少的一部分。信号和槽机制导致的这点性能损耗,对实时应用程序是可以忽略的。)
二、Qt信号和槽的本质是什么
回调函数。信号或是传递值,或是传递动作变化;槽函数响应信号或是接收值,或者根据动作变化来做出对应操作。
三、描述QT中的文件流(QTextStream)和数据流(QDataStream)的区别
文件流(QTextStream)。操作轻量级数据(int,double,QString)数据写入文本件中以后以文本的方式呈现。
数据流(QDataStream)。通过数据流可以操作各种数据类型,包括对象,存储到文件中数据为二进制。
文件流,数据流都可以操作磁盘文件,也可以操作内存数据。通过流对象可以将对象打包到内存,进行数据的传输。
四、描述QT的TCP通讯流程
服务端:(QTcpServer)
①创建QTcpServer对象
②监听list需要的参数是地址和端口号
③当有新的客户端连接成功回发送newConnect信号
④在newConnection信号槽函数中,调用nextPendingConnection函数获取新连接QTcpSocket对象
⑤连接QTcpSocket对象的readRead信号
⑥在readRead信号的槽函数使用read接收数据
⑦调用write成员函数发送数据
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
tcpServer = new QTcpServer;
tcpServer->listen(QHostAddress("192.168.0.111"),1234);
connect(tcpServer,SIGNAL(newConnection()),this,SLOT(new_connect()));
}
Widget::~Widget()
{
delete ui;
}
void Widget::new_connect()
{
qDebug("--new connect--");
QTcpSocket* tcpSocket = tcpServer->nextPendingConnection();
connect(tcpSocket,SIGNAL(readyRead()),this,SLOT(read_data()));
socketArr.push_back(tcpSocket);
}
void Widget::read_data()
{
for(int i=0; i<socketArr.size(); i++)
{
if(socketArr[i]->bytesAvailable())
{
char buf[256] = {};
socketArr[i]->read(buf,sizeof(buf));
qDebug("---read:%s---",buf);
}
}
}
客户端:(QTcpSocket)
①创建QTcpSocket对象
②当对象与Server连接成功时会发送connected 信号
③调用成员函数connectToHost连接服务器,需要的参数是地址和端口号
④connected信号的槽函数开启发送数据
⑤使用write发送数据,read接收数据
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
tcpSocket = new QTcpSocket;
connect(tcpSocket,SIGNAL(connected()),this,SLOT(connect_success()));
tcpSocket->connectToHost("172.20.10.3",1234);
}
Widget::~Widget()
{
delete ui;
}
void Widget::on_send_clicked()
{
std::string msg = ui->msg->text().toStdString();
int ret = tcpSocket->write(msg.c_str(),msg.size()+1);
qDebug("--send:%d--",ret);
}
void Widget::connect_success()
{
ui->send->setEnabled(true);
}
五、 描述UDP 之 UdpSocket通讯
UDP(User Datagram Protocol即用户数据报协议)是一个轻量级的,不可靠的,面向数据报的无连接协议。在网络质量令人十分不满意的环境下,UDP协议数据包丢失严重。由于UDP的特性:它不属于连接型协议,因而具有资源消耗小,处理速度快的优点,所以通常音频、视频和普通数据在传送时使用UDP较多,因为它们即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。所以QQ这种对保密要求并不太高的聊天程序就是使用的UDP协议。
在Qt中提供了QUdpSocket 类来进行UDP数据报(datagrams)的发送和接收。Socket简单地说,就是一个IP地址加一个port端口 。
流程:①创建QUdpSocket套接字对象 ②如果需要接收数据,必须绑定端口 ③发送数据用writeDatagram,接收数据用 readDatagram 。
六、多线程使用使用方法
方法一:①创建一个类从QThread类派生②在子线程类中重写 run 函数, 将处理操作写入该函数中 ③在主线程中创建子线程对象, 启动子线程, 调用start()函数
方法二:①将业务处理抽象成一个业务类, 在该类中创建一个业务处理函数②在主线程中创建一QThread类对象 ③在主线程中创建一个业务类对象 ④将业务类对象移动到子线程中 ⑤在主线程中启动子线程 ⑥通过信号槽的方式, 执行业务类中的业务处理函数
多线程使用注意事项:
* 1. 业务对象, 构造的时候不能指定父对象
* 2. 子线程中不能处理ui窗口(ui相关的类)
* 3. 子线程中只能处理一些数据相关的操作, 不能涉及窗口
七、多线程下,信号槽分别在什么线程中执行,如何控制
可以通过connect的第五个参数进行控制信号槽执行时所在的线程
connect有几种连接方式,直接连接和队列连接、自动连接
直接连接(Qt::DirectConnection):信号槽在信号发出者所在的线程中执行
队列连接 (Qt::QueuedConnection):信号在信号发出者所在的线程中执行,槽函数在信号接收者所在的线程中执行
自动连接 (Qt::AutoConnection):多线程时为队列连接函数,单线程时为直接连接函数。
8.static和const的使用
1.static:静态变量声明,分为局部静态变量,全局静态变量,类静态成员变量。也可修饰类成员函数。
局部静态变量:存储在静态存储区,程序运行期间只被初始化一次,作用域仍然为局部作用域,在变量定义的函数或语句块中有效,程序结束时由操作系统回收资源。
全局静态变量:存储在静态存储区,静态存储区中的资源在程序运行期间会一直存在,直到程序结束由系统回收。未初始化的变量会默认为0,作用域在声明他的文件中有效。
类静态成员变量:被类的所有对象共享,包括子对象。必须在类外初始化,不可以在构造函数内进行初始化。
类静态成员函数:所有对象共享该函数,不含this指针,不可使用类中非静态成员。
2.const:常量声明,类常成员函数声明。
const和static不可同时修饰类成员函数,const修饰成员函数表示不能修改对象的状态,static修饰成员函数表示该函数属于类,不属于对象,二者相互矛盾。const修饰变量时表示变量不可修改,修饰成员函数表示不可修改任意成员变量。难记点(我是感觉很SB,除了面试,实际工作中完全没遇到):
const char* p = new char('a'): 表示p指向的内容不可修改但是p可修改。*p = 'b'; // 错误 p = p2;// 正确指针可修改
char* const p = new char('a'): 表示p不可修改,但是p指向的内容可修改。*p= 'b';// 正确 p = p2;//错误
9.指针和引用的异同
指针:是一个变量,但是这个变量存储的是另一个变量的地址,我们可以通过访问这个地址来修改变量。
引用:是一个别名,还是变量本身。对引用进行的任何操作就是对变量本身进行的操作。
相同点:二者都可以对变量进行修改。
不同点:指针可以不必须初始化,引用必须初始化。指针可以有多级,但是引用只有一级(int&& a不合法, int** p合法)。指针在初始化后可以改变,引用不能进行改变,即无法再对另一个同类型对象进行引用。sizeof指针可以得到指针本身大小,sizeof引用得到的是变量本身大小。指针传参还是值传递,引用传参传的是变量本身。
10.常用数据结构
1.vector:向量,连续存储,可随机访问。
2.deque:双向队列,连续存储,随机访问。
3.list:链表,内存不连续,不支持随机访问。
4.stack:栈,不可随机访问,只允许再开头增加/删除元素。
5.queue:单向队列,尾部增加,开头删除。
6.set:集合,采用红黑树实现,可随机访问。查找、插入、删除时间复杂度为O(logn)。
7.map:图,采用红黑树实现,可随机访问。查找、插入、删除时间复杂度为O(logn)。
8.hash_set:哈希表,随机访问。查找、插入、删除时间复杂读为O(1)。
11.Tcp
1.三次握手:建立一个TCP连接时,需要客户端服务端总共发送三个包以确认连接的建立。在这一过程中由客户端执行connect来触发,流程如下:
2. 四次挥手:断开一个Tcp连接时,需要客户端和服务端总共发送四个包以确认连接的端口。在socket编程中,这一过程由客户端或服务端任一方执行close来触发,流程如下:
12.Qt多线程同步的几种实现方式
(1)互斥量:QMutex
QMutex类提供的是线程之间的访问顺序化。QMutex的目的是保护一个对象/数据结构或者代码段在同一时间只有一个线程可以访问。基本使用方法如下:
QMutex mutex;
int var;
void function()
{
mutex.lock();
// 访问var
var * var;
mutex.unlock();
}
如果使用mutex加锁,却没有使用unlock解锁,那么就会造成死锁,其他线程永远也得不到访问变量的机会,所以为了解决这个问题,Qt引入了QMutexLocker类,二者直接可以配合使用更加方便简洁,示例如下:
QMutex mutex;
int var;
void function()
{
QMutextLocker locker(&mutex);
// 访问var
var * var;
}
(2)QReadWriteLock
QMutex只允许某个时刻有一个线程对共享资源进行访问,如果需要多个线程对共享资源进行读访问,同时只有一个线程进行写访问,这种情况下就可以使用QReadWriteLock。QReadWriteLock主要实现多个线程读资源,一个线程写。写线程执行的时候会阻塞所有的读线程,而读线程之间的运行不需要进行同步。使用示例如下:
int var;
QReadWriteLock lock;
void function()
{
lock.lockForRead();
int x = var;
lock.unlock();
}
void function2()
{
lock.lockForWrite();
var = 100;
lock.unlock();
}
和QMutexLocker一样,Qt同样提供了QReadLocker和QWriteLocker。
int var;
QReadWriteLock lock;
void fun()
{
QReadLocker(&lock);
int x = var;
}
void fun2()
{
QWriteLocker(&lock);
var = 1000;
}
(3)QSemaphore
QSemaphore是QMutex的一般化,它可以保护一定数量的相同资源,而QMutex只能保护一个资源。信号量比互斥量具有更好的并发性,我们可以利用信号量实现生产者-消费者模式,如下所示:
const int dataSize = 100000;
const int bufferSize = 1024;
char buffer[bufferSize];
QSemaphore freeBytes(bufferSize);
QSemaphore usedButes;
void Producer::run()
{
for (int i = 0; i < dataSize; ++i)
{
freeBytes.acquire();
buffer[i % bufferSize] = i;
usedBytes.release();
}
}
void Consumer::run()
{
for (int i = 0; i < dataSize; ++i)
{
usedBytes.acquire();
qDebug() << buffer[i % bufferSize];
freeBytes.release();
}
}
标签:const,变量,int,面经,C++,指针,函数 来源: https://www.cnblogs.com/xcb-1024day/p/16667919.html