1、介绍一下static的各种用法,static修饰的变量在别的文件中可以使用吗
Static修饰全局变量叫做静态全局变量,
Static修饰局部变量叫做静态局部变量,
Static修饰函数叫做静态函数;
静态全局变量:限制变量的作用域,仅在本文件中访问,其他文件不可访问;
静态局部变量:仅在本函数体内访问,本文件其他函数体内不可访问;但静态局部变量的值在程序运行期间不会销毁;
静态函数:仅在本文件中调用,其他文件中不可调用,即程序员不用担心编写的函数与其他文件的函数同名。
总的来说:
(1)在修饰变量的时候,static修饰的静态局部变量只执行初始化一次,而且延长了局部变量的生命周期,直到程序运行结束以后才释放。
(2)static修饰全局变量的时候,这个全局变量只能在本文件中访问,不能在其它文件中访问,即便是extern外部声明也不可以。
(3)static修饰一个函数,则这个函数的只能在本文件中调用,不能被其他文件调用。Static修饰的变量存放在全局数据区的静态变量区,包括全局静态变量和局部静态变量,都在全局数据区分配内存。初始化的时候自动初始化为0。
(4)不想被释放的时候,可以使用static修饰。比如修饰函数中存放在栈空间的数组。如果不想让这个数组在函数调用结束释放可以使用static修饰。
(5)考虑到数据安全性(当程序想要使用全局变量的时候应该先考虑使用static)
2、static修饰类成员函数有什么作用
static修饰的函数叫做静态函数,静态函数有两种,根据其出现的地方来分类:
如果这个静态函数出现在类里,那么它是一个静态成员函数;
静态成员函数的作用在于:调用这个函数不会访问或者修改任何对象(非static)数据成员。
其实很好理解,类的静态成员(变量和方法)属于类本身,在类加载的时候就会分配内存,可以通过类名直接去访问;非静态成员(变量和方法)属于类的对象,所以只有在类的对象产生(创建类的实例)时才会分配内存,然后通过类的对象(实例)去访问。
如果它不是出现在类中,那么它是一个普通的全局的静态函数。
这样的static函数与普通函数的区别是:**用static修饰的函数,限定在本源码文件中,不能被本源码文件以外的代码文件调用。**而普通的函数,默认是extern的,也就是说它可以被其它代码文件调用。
在函数的返回类型前加上关键字static,函数就被定义成为静态函数。普通 函数的定义和声明默认情况下是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。因此定义静态函数有以下好处:
<1> 其他文件中可以定义相同名字的函数,不会发生冲突。 <2> 静态函数不能被其他文件所用。
3、const用法,const与define区别,const指针
C++ const 允许指定一个语义约束,编译器会强制实施这个约束,允许程序员告诉编译器某值是保持不变的。如果在编程中确实有某个值保持不变,就应该明确使用const,这样可以获得编译器的帮助。
1、const与基本数据类型
int x = 3;//变量
const int x = 3;//常量
2、const修饰指针变量时:
(1)只有一个const,如果const位于*左侧,表示指针所指数据是常量,不能通过解引用修改该数据;指针本身是变量,可以指向其他的内存单元。
(2)只有一个const,如果const位于*右侧,表示指针本身是常量,不能指向其他内存地址;指针所指的数据可以通过解引用修改。
(3)两个const,*左右各一个,表示指针和指针所指数据都不能修改。
3、const与引用
int x = 3;
const int &y = x;
x = 10;//正确
y = 20;//错误,const修饰x的别名y,不能更改
例1:
const int x = 3;
int *y = &x; //erro
x是不可变的,但是我们定义的指针是可变的,可变的指针去指向一个不可变的变量,就会存在风险,会通过指针y来改变x的值;权限大的去接受权限小的,是不可行
二、const与#define的区别
(1) 编译器处理方式不同
define宏是在预处理阶段展开。
const常量是编译运行阶段使用。
(2) 类型和安全检查不同
define宏没有类型,不做任何类型检查,仅仅是展开。
const常量有具体的类型,在编译阶段会执行类型检查。
(3) 存储方式不同
define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存。(宏定义不分配内存,变量定义分配内存。)
const常量会在内存中分配(可以是堆中也可以是栈中)。
(4)const 可以节省空间,避免不必要的内存分配。 例如:
#define PI 3.14159 //常量宏
const doulbe Pi=3.14159; //此时并未将Pi放入ROM中 …
double i=Pi; //此时为Pi分配内存,以后不再分配!
double I=PI; //编译期间进行宏替换,分配内存
double j=Pi; //没有内存分配
double J=PI; //再进行宏替换,又一次分配内存!
const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝(因为是全局的只读变量,存在静态区),而 #define定义的常量在内存中有若干个拷贝。
(5) 提高了效率。 编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。
(6) 宏替换只作替换,不做计算,不做表达式求解;
宏预编译时就替换了,程序运行时,并不分配内存。
const 与 #define的比较
C++ 语言可以用const来定义常量,也可以用 #define来定义常量。但是前者比后者有更多的优点:
(1) const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应)。
(2) 有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试。
4、指针和引用的区别
精简版:
指针:变量,独立,可变,可空,替身,无类型检查;
引用:别名,依赖,不变,非空,本体,有类型检查;
完整版:
概念
指针从本质上讲是一个变量,变量的值是另一个变量的地址,指针在逻辑上是独立的,它可以被改变的,包括指针变量的值(所指向的地址)和指针变量的值对应的内存中的数据(所指向地址中所存放的数据)。
引用从本质上讲是一个别名,是另一个变量的同义词,它在逻辑上不是独立的,它的存在具有依附性,所以引用必须在一开始就被初始化(先有这个变量,这个实物,这个实物才能有别名),而且其引用的对象在其整个生命周期中不能被改变,即自始至终只能依附于同一个变量(初始化的时候代表的是谁的别名,就一直是谁的别名,不能变)。
(1)指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元,即指针是一个实体;而引用跟原来的变量实质上是一个东西,只不过是原变量的一个别名而已。如:
int a = 1; int *p = &a;
int a = 1; int &b = a;
1
2
上面定义了一个整型变量和一个指针变量p,该指针变量指向a的存储单元,即p的值是a存储单元的地址。
而下面2句定义了一个整型变量a和这个整型a的引用b,事实上a和b是同一个东西,在内存占有同一个存储单元。
(2)可以有const指针,但是没有const引用;
(3)指针可以有多级,但是引用只能是一级(int **p;合法 而 int &&a是不合法的);
(4)指针的值可以为空,但是引用的值不能为NULL,并且引用在定义的时候必须初始化;
(5)指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了,从一而终。
(6)sizeof引用得到的是所指向的变量(对象)的大小,而sizeof指针得到的是指针本身的大小;
(7)指针和引用的自增(++)运算意义不一样;
二、相同点
都是地址的概念;
指针指向一块内存,它的内容是所指内存的地址;
引用是某块内存的别名。
5、什么情况会用到引用传递
引用传递与值传递的区别就是:引用传递的时候,操作的是同一个对象,对现在的操作会改变原来的对象的值;而值传递的时候,操作的是原来对象的一个拷贝。对现对象的改变不会改变原来的对象的值
C++中的指针参数传递和引用参数传递
指针参数传递本质上是值传递,它所传递的是一个地址值。值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,会在栈中开辟内存空间以存放由主调函数传递进来的实参值,从而形成了实参的一个副本(替身)。值传递的特点是,被调函数对形式参数的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值(形参指针变了,实参指针不会变)。
引用参数传递过程中,被调函数的形式参数也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参(本体)的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量(根据别名找到主调函数中的本体)。因此,被调函数对形参的任何操作都会影响主调函数中的实参变量。
引用传递和指针传递是不同的,虽然他们都是在被调函数栈空间上的一个局部变量,但是任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。而对于指针传递的参数,如果改变被调函数中的指针地址,它将应用不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量(地址),那就得使用指向指针的指针或者指针引用。
从编译的角度来讲,程序在编译时分别将指针和引用添加到符号表上,符号表中记录的是变量名及变量所对应地址。指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值(与实参名字不同,地址相同)。符号表生成之后就不会再改,因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能修改。
6、为什么在.h文件中使用#ifdef……
***这是一种防止头文件被多次包含的预处理技术,***由于各种原因可能是有问题的。在编译项目时,编译每个.cpp文件(通常)。简单来说,这意味着编译器会把你的.cpp文件,打开任何文件#included,将它们全部连接成一个海量文本文件,然后执行语法分析,最后将它转换成一些中间代码,优化/执行其他任务,最后生成目标架构的汇编输出。因此,如果#included一个.cpp文件下的文件多次,则编译器将附加文件内容两次,因此如果该文件中有定义,你会收到一个编译器错误,告诉你重新定义了一个变量。FILE_H当编译过程中的预处理器步骤处理文件时,首次到达其内容时,前两行将检查是否已为预处理器定义。如果没有,它将定义FILE_H并继续处理它和指令之间的代码#endif。下一次该文件的内容被预处理器看到时,检查FILE_H将是假的,所以它将立即扫描#endif并继续。这样可以防止重新定义错误。它将定义并继续处理它和指令之间的代码
7、什么函数不能定义为虚函数
常见的不能声明为虚函数的有:普通函数(非成员函数);静态成员函数;内联成员函数;构造函数;友元函数。
1.为什么C++不支持普通函数为虚函数?
普通函数(非成员函数)只能被overload,不能被override,声明为虚函数也没有什么意思,因此编译器会在编译时邦定函数。
2.为什么C++不支持构造函数为虚函数?
这个原因很简单,主要是从语义上考虑,所以不支持。因为构造函数本来就是为了明确初始化对象成员才产生的,然而virtual function主要是为了再不完全了解细节的情况下也能正确处理对象。另外,virtual函数是在不同类型的对象产生不同的动作,现在对象还没有产生,如何使用virtual函数来完成你想完成的动作。(这不就是典型的悖论)
3.为什么C++不支持内联成员函数为虚函数?
其实很简单,那内联函数就是为了在代码中直接展开,减少函数调用花费的代价,虚函数是为了在继承后对象能够准确的执行自己的动作,这是不可能统一的。(再说了,inline函数在编译时被展开,虚函数在运行时才能动态的邦定函数)
4.为什么C++不支持静态成员函数为虚函数?
这也很简单,静态成员函数对于每个类来说只有一份代码,所有的对象都共享这一份代码,他不归某个具体对象所有,所以他也没有要动态邦定的必要性。
5.为什么C++不支持友元函数为虚函数?
因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。
8、智能指针作用及分类
智能指针是一个类,这个类的构造函数中传入一个普通指针,析构函数中释放传入的指针。智能指针的类都是栈上的对象,所以当函数(或程序)结束时会自动被释放。
作用
C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。
智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。
分类:
auto_ptr
在 C++ 语言中,要使用 STL 中的 auto_ptr 对象,必须包含头文件 memory,该文件包括 auto_ptr 模板。使用通常的模板句法来实例化所需类型的指针。auto_ptr 构造函数是显式的,不存在从指针到 auto_ptr 对象的隐式类型转换。
auto_ptr(c++98的方案,c++11已经抛弃),采用的是所有权模式。
模板可以通过构造函数将 auto_ptr 对象初始化为一个常规指针。auto_ptr 是一个智能指针,但其特性远比指针要多。值得注意的是,在使用 auto_ptr 时,只能配对使用 new 和 delete。
提示,只能对 new 分配的内存使用 auto_ptr 对象,不要对由 new() 分配的或通过声明变量分配的内存使用它。
unique_ptr
unique_ptr实现独占式拥有或者严格拥有概念,保证同一时间只有一个智能指针可以指向该对象。它对于避免资源泄露特别有用。
shared_ptr
shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在最后一个引用被销毁时候释放。通过use_count()来查看资源被几个指针共享。除了可以通过new来构造,还可以通过传入auto_ptr,unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0,资源会被释放。
成员函数:
use_count()返回引用计数的个数;
unique()返回是否独占所有权;
swap()交换两个shared_pt6;
reset()放弃内部对象的所有权或者拥有对象的变更,会引起原来对象的引用计数的减少;
get()返回内部对象的地址。
weak_ptr
weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。
成员函数比shared_ptr多了两个,但是少了get():
expired() 为use_count()为0,返回true,否则返回false;
lock()如果expired为空,返回空的shared_ptr;否则返回一个指向对象的shared_ptr;
总结
不要把一个原生指针给多个智能指针对象管理, 对所有的智能指针对象都成立。
不要把 this 指针给智能指针对象,对所有的智能指针对象(包括 auto_ptr)都成立。
不要在函数实参里创建智能指针对象。
处理不是 new 创建的对象要小心. 如果确实需要这样做, 需要智能指针传递一个删除器, 自定义删除行为。
不要使用 new 创建一个智能指针对象.如 new shared_ptr 。
使用 dynamic_pointer_cast 进行转换。
不要 memcpy 智能指针对象。
智能指针对象数组的使用, 需要自定义释放器。
将智能指针对象作为函数参数传递时要小心, 如下面的代码, 当调用所在的表达式结束(即函数调用返回)时, 这个临时对象就被销毁了, 它所指向的内存也被释放.。
当将一个智能指针对象(如 shared_ptr)绑定到一个普通指针时, 就将内存管理的责任交给了这个 shared_ptr. 此后就不应该使用内置指针来访问 shared_ptr 所指向的内存了。
不能使用 delete 释放 get 返回的普通指针. get 函数的设计是为了向不能使用智能指针的代码传递一个普通指针, 应该减少 get 函数的调用。
不要使用 get 返回的普通指针来初始化另一个智能指针, 或为另一个智能指针赋值. 显然如果这样做, 将导致两次释放相同的内存, 或者其中一个已经将内存释放, 但另一个还在使用。
9、结构体与类、联合体的区别
首先一句话——在C++中,结构体和类没有什么区别,唯一的区别就是:默认的访问权限和继承访问权限不同。其他的,类能怎么干,结构体也能怎么干!
默认访问权限:结构体是public,类是private
默认继承访问权限:结构体是public,类是private
C++ primer 里面讲,union是一种特殊的节省空间的类。
一个union可以有多个数据成员,但是在任意时刻只能有一个数据成员有值。
也就是说,当我们给某一个数据成员赋值之后,该union的其他数据成员就变成未定义的状态了。
分配给一个union的存储空间至少要容纳他的最大的数据成员。
union与class类的区别:
1、与结构体一样,union的成员的默认访问权限是public。
2、union可以定义包括构造函数和析构函数在内的成员函数。
3、既不能作为基类被继承也不能继承他人,union是独立的,所以也不能实现虚函数。
注意的点:
1、union的数据成员可以是类类型, 但不能含有引用类型的成员,因为union的所有数据成员是要共享内存的。
2、因为一旦给union的一个成员赋值的时候,其他的值就变成未定义的状态啦,所以在使用union的时候必须知道当前union中存的是什么类型值。
10、OSI七层协议
我们都知道互联网的本质是一系列的网络协议,这个协议就叫做OSI(Open System Interconnect——开放式系统互联的含义)协议。
按照功能不同分工不同,人为的分为七层。实际上这七层是并不存在的,也就是说没有这些概念,而我们今天提到的七层概念,只是人为的划分而已。目的是为了让我们更好地理解这些都是用来做什么的。
物理层
是参考模型的最底层。该层是网络通信的数据传输介质,由连接不同结点的电缆与设备共同构成。
主要功能是: 利用传输介质为数据链路层提供物理连接,负责处理数据传输并监控数据出错率,以便数据流的透明传输。
数据链路层
是参考模型的第二层。
主要功能是: 在物理层提供的服务基础上,在通信的实体间建立数据链路连接,传输以"帧"为单位的数据包,并采用差错控制与流量控制方法,使用差错的物理线路变成无差错的数据链路。
网络层
是参考模型的第三层。
主要功能是: 为数据在节点之间传输创建逻辑链路,通过路由选择算法为分组通过通信子网选择最适当的路径,以及实现拥塞控制、网络互连等功能。
传输层
是参考模型的第四层。
主要功能是: 向用户提供可靠地端到端服务,处理数据包错误、数据包次序,以及其他一些关键传输问题。传输层向高层屏蔽了下层数据通信的细节。因此,它是计算机通信体系结构中关键的一层。
会话层
是参考模型的第五层。
主要功能是: 负责维护两个结点之间的传输连接,以便确保点到点传输不中断,以及管理数据交换等功能。
表示层
是参考模型的第六层
主要功能: 用于处理在两个通信系统中交换信息的表示方法,主要包括数据格式变换、数据加密与解密、数据压缩与恢复等功能。
应用层
是参考模型的最高层。
主要功能是: 为应用软件提供了很多服务,比如文件服务器、数据库服务、电子邮件与其他网络软件服务。
11、三次握手
三次握手(Three-way Handshake)其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备。实质上其实就是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号,交换TCP窗口大小信息。
刚开始客户端处于 Closed 的状态,服务端处于 Listen 状态。
进行三次握手:
第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN。此时客户端处于 SYN_SENT 状态。
首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。
第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s)。同时会把客户端的 ISN + 1 作为ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_RCVD 的状态。
在确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y。
第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。
确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。
发送第一个SYN的一端将执行主动打开(active open),接收这个SYN并发回下一个SYN的另一端执行被动打开(passive open)。
12、进程与线程区别
进程与线程的区别
线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process),它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。
根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的
影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行
13、判断链表有环
暴力法、哈希表、快慢指针
14、平衡二叉树定义
如果在二叉查找树中一开始给定的数列是有序的,那么在构建二叉查找树时就会形成一条很长的链条式的树,此时对这棵树的查找复杂度将会变成O(n),起不到使用二叉查找树来进行数据查询优化的目的,于是需要对这颗树的结构进行调整,使树的高度在每次插入元素后仍然能保持O(logn)的级别,这样能让查询的时间仍然是O(logn),于是就产生了平衡二叉树。
平衡二叉树由前苏联两位数学家提出,因此一般也称为AVL树,AVL树仍然是一颗二叉查找树,只是在其基础上增加了“平衡”的要求。所谓平衡是指,对AVL树的任意结点来说,其左子树与右子树的高度之差的绝对值不超过1,其中左子树与右子树的高度之差称为该结点的平衡因子
只要能随时保证每个结点的平衡因子的绝对值不超过1, AVL的高度就始终能保持O(logn)级别,由于需要对每个结点都得到平衡因子,因此需要在树的结构中加入一个变量height,用来记录以当前结点为根结点的子树的高度。
15、用两个栈实现一个队列
实现队列
template<typename T> class CQueue{public:CQueue(void);~CQueue(void);void appendTail(const T& node);T deleteHead();private:stack<T> stack1;stack<T> stack2;};template<typename T> void CQueue<T>::appendTail(const T& element)//尾插{stack1.push(element);}template<typename T> T CQueue<T>::deleteHead(){if (stack2.size() <= 0){while (stack1.size > 0){T& data = stack1.top();stack1.pop();stack2.push(data);}}T head = stack2.top();stack2.pop();if (stack2.size() == 0)//当stack2为空时,抛异常throw new exception("queue is empty");return head;}
如果觉得《招商银行/招银网络科技面经 答案》对你有帮助,请点赞、收藏,并留下你的观点哦!