链接: https://www.nowcoder.com/discuss/18270?type=2&order=0&pos=30&page=13
来源:牛客网
数据库:
范式
第一范式:数据库表的每一项都是不可分割的原子数据项,不能是集合。比如班级信息表里面不能有班级的学生。
第二范式:在第一范式的基础上,所有属性完全依赖于主键,完全依赖就是不能取决于主键的一部分
第一范式->第二范式->第三范式
→→→数据冗余越来越少,查询越来越复杂
事 务
1原子性
2一致性:
使数据库从一个一致性状态到另一个一致性状态
3隔离性:
一个事物的执行不被其他事务干扰
4永久性:
常用 SQL语句
分组查询(avg max min)、复杂连接查询、嵌套查询、结果排序(逆序ansc)、
操作系统
错题
虚存=min (主存+辅存,逻辑地址)
进程和线程
进程是程序的一次执行,包括代码和数据,是CPU分配资源的基本单位,一个进程可以包括多个线程。进程之间通信方式:管道、SOCKET、信号量(互斥、同步)等。
进程的调度算法
文件系统
内存分配策略
计算机网络
HTTP协议
基于TCP协议的应用层协议
H TTP是一个普通用在浏览器和web服务器之间进行数据交换的 文本 协议
HTTP协议的ETAG响应头主要用于信息的过期验证
HTML错误代码
(1) 常见错误代码:
200服务器成功返回了网页,成功处理了请求
304未修改,自从上次请求后,请求的页面未被修改过,此时服务器不会返回网页内容,节省带宽和开销
404请求的网页不存在
500服务器内部错误
503服务器暂时不可用(超载、停机维护),通常只是暂时状态
(2) 1xx:临时响应,服务器端响应成功, 等待请求者进一步操作
(3) 2xx:响应成功
202接受请求,未处理
204处理了请求,但没有返回任何内容
(4) 3xx重定向,要完成响应,服务器需要进一步处理
301网页已被永久移动到新位置
302临时移动到新位置
305要求只能使用代理才能访问
(5) 4xx请求错误
400不理解请求语法
401要求身份验证,先登陆才能请求
403禁止访问,服务器拒绝请求
405请求中的方法被禁用
408请求超时
(6) 5xx服务器在处理请求时内部发生错误,来自服务器本身的错误
501服务器不具备完成该请求的功能
502服务器作为网关或代理,从上游服务器收到无效响应
504网关超时
TCP/IP与UDP
TCP与UDP
TCP面向连接、可靠的数据传输,有拥塞控制和流量控制,大量数据,速度慢
TCP建立连接的三次握手
详细过程和状态变化
TCP关闭连接的四次挥手
FIN- ->
<--FINACK
ß FIN
如何提高UDP的可靠性
应用层 实现超时重传,客户端发送一个包,服务器返回一个包表示收到,如果客户端超过一定时间没有收到该包,就需要重传
备用
SOCKET编程
当recv函数在接受数据时是阻塞的,当返回值<0,说明连接出错
当返回值=0,表示对端关闭了连接
返回值>0,接受到的数据的大小
TCP/IP分层,各层作用
应用层:为操作系统、应用程序提供访问网络的接口(Telnet、FTP、HTTP、SNMP、DNS域名解析)
(表示层)
(会话层)
传输层:两点之间的根据使用的协议(TCP、UDP),传输相应数据报文,
网络层:整个网络的传输路径选择,路由(IP、RIP)
网络访问层:数据链路层、物理层的结合,数据链路层相邻节点之间数据帧传输,物理层就是光纤上比特级的数据传输。
PING操作的原理
使用ICMP,在IP主机、路由器之间传递控制消息
网络层的协议
IP协议 根据IP地址决定转发、路由的协议
ICMP本质理解为带差错报告的IP协议,在主机和路由器之间传递控制信息(网络通不通,主机可不可达,路由可不可达到)
ARP:将IP地址转化为MAC地址
RARP:物理地址转为IP地址
传输层的协议
TCP
UDP
应用层协议
HTTP
SMTP:简单邮件传输协议
DNS:
Telnet
FTP:文件传输协议
WWW:
DNS的完整流程(域名->IP地址)
DNS采用 分布式 的域名系统,减少故障发生
当一个应用需要把主机名解析为IP地址时,该应用程序就会调用解析程序,把待解析的域名放在DNS请求报文中,以 UDP数据报 方式发送给本地域名服务器,本地服务器在查找域名后,把对应的IP地址放在回答报文中返回,应用程序获得目的主机的IP地址后即可进行通信。若本地域名服务器不能解析该域名,则向上级域名服务器继续发送查询请求,直到可以解析为止。
备用
HTTP1.0/1.1区别
HTTP1.1中才有 cache-control响应头,主要用于控制信息在浏览器的缓存
1. HTTP 1.0规定浏览器与服务器只保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接,服务器完成请求处理后立即断开TCP连接,服务器不跟踪每个客户也不记录过去的请求
缺陷:访问一个包含有许多图像的网页文件的整个过程包含了多次请求和响应,每次请求和响应都需要建立一个单独的连接,每次连接只是传输一个文档和图像,器端每次建立和关闭连接却是一个相对比较费时的过程,并且会严重影响客户机和服务器的性能
2. HTTP 1.1支持持久连接,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟
HTTP 1.1还允许客户端不用等待上一次请求结果返回,就可以发出下一次请求,但服务器端必须按照接收到客户端请求的先后顺序依次回送响应结果
HTTP 1.1还提供了Host、身份认证、状态管理和Cache缓存等机制相关的请求头和响应头
编程经验
C++
一定要弄懂c++的内存分配机制,父类,子类继承时的内存如何分配,封装、继承、多态、虚函数的实现机制和原理。
小细节
实现String类
String类的原型如下
class String
{
public:
String(const char *str=NULL); //构造函数
String(const String &other); //拷贝构造函数
~String(void); //析构函数
String& operator=(const String &other); //等号操作符重载
ShowString();
private:
char *m_data; //指针
};
String::~String()
{
delete [] m_data; //析构函数,释放地址空间
}
String::String(const char *str)
{
if (str==NULL)//当初始化串不存在的时候,为m_data申请一个空间存放'\0';
{
m_data=new char[1];
*m_data='\0';
}
else//当初始化串存在的时候,为m_data申请同样大小的空间存放该串;
{
int length=strlen(str);
m_data=new char[length+1];
strcpy(m_data,str);
}
}
String::String(const String &other)//拷贝构造函数,功能与构造函数类似。
{
int length=strlen(other.m_data);
m_data=new [length+1];
strcpy(m_data,other.m_data);
}
String& String::operator =(const String &other)
{
if (this==&other)//当地址相同时,直接返回;
return *this;
delete [] m_data;//当地址不相同时,删除原来申请的空间,重新开始构造;
int length= strlen (other.m_data);
m_data=new [length+1];
strcpy(m_data,other.m_data);
return *this;
}
String::ShowString()//由于m_data是私有成员,对象只能通过public成员函数来访问;
{
cout<<this->m_data<<endl;
}
Str cmp strcpy的返回类型
C++如何限制类对象只能静态分配或者只能只能动态分配
动态分配就是用运算符new来创建一个类的对象,在堆上分配内存。
(1)动态分配(在堆上分配内存)
将类的构造函数和析构函数设为protected属性,这样类对象不能够访问,但是派生类能够访问,能够正常的继承。同时创建另外两个create和destory函数类创建对象。(将create设为static原因是:创建对象的时候是A *p = A::create();只有静态成员函数才能够通过类名来访问。)
[cpp] view plain copy
1. class A
2. {
3. protected :
4. A(){}
5. ~A(){}
6. public :
7. static A* create()
8. {
9. return new A();
10. }
11. void destory()
12. {
13. delete this ;
14. }
15. };
(2)静态分配(在栈上)
把new、delete运算符重载为private属性就可以了。
[cpp] view plain copy
1. class A
2. {
3. private :
4. void * operator new ( size_t t){} // 注意函数的第一个参数和返回值都是固定的
5. void operator delete ( void * ptr){} // 重载了new就需要重载delete
6. public :
7. A(){}
8. ~A(){}
9. };
实现strcpy()
char * strcpy(char* dest, const char* src)
{
assert(dest!=NULL&&src!=NULL);
char* res=dest;
while((*dest++=*src++)!=’\0’);
return res;
}
内联函数与宏
用内联取代宏:
1.内联函数在运行时可调试,而宏定义不可以;
2.编译器会对内联函数的参数类型做安全检查或自动类型转换(同普通函数),而宏定义则不会;
3.内联函数可以访问类的成员变量,宏定义则不能;
4.在类中声明同时定义的成员函数,自动转化为内联函数。
抽象类、接口
抽象类是包含纯虚函数的类
虚继承
作用: 为了解决从不同途径继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题,将共同基类设置为虚基类。
这时从不同的路径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射。这样不仅就解决了二义性问题,也节省了内存,避免了数据不一致的问题。
底层实现原理: 底层实现原理与编译器相关,一般通过虚基类指针实现,即各对象中只保存一份父类的对象,多继承时通过
虚基类指针引用该公共对象,从而避免菱形继承中的二义性问题。
多继承的二义性
构造函数中可不可以抛出异常?析构函数呢?
理论上都可以抛出异常。
但析构函数最好不要抛出异常,将会导致析构不完全,从而有内存泄露
构造函数和析构函数中调用虚函数
可以,虚函数底层实现原理
C++中的空类,默认产生哪些类成员函数
class Empty
{
public:
Empty(); // 缺省构造函数
Empty( const Empty& ); // 拷贝构造函数
~Empty(); // 析构函数
Empty& operator=( const Empty& ); // 赋值运算符
Empty* operator&(); // 取址运算符
const Empty* operator&() const; // 取址运算符 const
};
函数指针
Int and(int a,int b){return a+b;}
int (*p)(int ,int );
p=and;
int c=(*p)(1,2);
Int型变量作为bool使用
除了 0是false其余都为true
Int a=-2;while(a++);//执行2次
int c=-2;
int a=c||0;//a=1
面向对象的特性
抽象、继承、封装、多态
重载运算符
. * :: sizeof ?: 不能重载
const A operator +(A &b)
{
return A(this.data+b.data)
}
指针和引用
★ 区别:
1.指针是一个实体,而引用仅是个别名;
2.引用使用时无需解引用(*),指针需要解引用;
3.引用只能在定义时被初始化一次,之后不可变;指针可变;
4.引用没有const,指针有const;
5.引用不能为空,指针可以为空;
6.“sizeof引用”得到的是所指向的变量(对象)的大小,而“sizeof指针”得到的是指针本身(所指向的变量或对象的地址)的大小;
7.指针和引用的自增(++)运算意义不一样;
8.从内存分配上看:程序为指针变量分配内存区域,而引用不需要分配内存区域
基类析构函数为虚函数
在实现多态时,当用基类操作派生类,在析构时防止只析构基类而不析构派生类的状况发生,如果只调用基类的析构函数,则派生类没有释放掉,造成内存泄漏
C++11
a uto i= 10;
auto s=” hello ”;
强制类型转换符
dynamic_cast
该转换符用于 将一个指向派生类的基类指针或引用转换为派生类的指针或引用
//B 是D的基类,D是派生类
B* pb;
D* pd,md ;
pb = &md;
pd=dynamic_cast<D *>(pb);
把指向派生类D的基类指针pb转换为派生类D的指针,然后将这个指针赋给派生类D的指针pd。
如果指向派生类的基类指针B想访问派生类D中的除虚函数之外的成员时就需要把该指针转换为指向派生类D的指针,以达到访问派生类D中特有的成员的目的,比如派生类D中含有特有的成员函数g(),这时可以这样来访问该成员 dynamic_cast<D *>(pb)->g(); 因为dynamic_cast转换后的结果是一个指向派生类的指针,所以可以这样访问派生类中特有的成员。但是该语句不影响原来的指针的类型,即基类指针pb仍然是指向基类B的。如果单独使用该指针仍然不能访问派生类中特有的成员。
dynamic_cast的注意事项
dynamic_cast转换符只能用于指针或者引用。dynamic_cast转换符只能用于含有虚函数的类。dynamic_cast转换操作符在执行类型转换时首先将检查能否成功转换,如果能成功转换则转换之,如果转换失败,如果是指针则反回一个0值,如果是转换的是引用,则抛出一个bad_cast异常,所以在使用dynamic_cast转换之间应使用if语句对其转换成功与否进行测试,比如 pd = dynamic_cast(pb); if(pd){…}else{…} ,或者这样测试 if(dynamic_cast(pb)){…}else{…} 。
因此, dynamic_cast操作符一次执行两个操作。 首先验证被请求的转换是否有效,只有转换有效,操作符才实际进行转换。基类的指针可以赋值为指向派生类的对象,同样,基类的引用也可以用派生类对象初始化,因此,dynamic_cast操作符执行的验证必须在运行时进行。
const_cast操作符
该操作符用于改变const和volatile, const_cast最常用的用途就是删除const属性;
操作符不能改变类型的其他方面,他只能改变const或volatile,即const_cast不能把int改变为double,但可以把const int改变为int。const_cast只能用于指针或引用。
int a=3; const int *b=&a; int* c=const_cast(b); *c=4; cout<<a<<*c; 输出为两个4
static_cast操作符
该操作符用于非多态类型的转换,任何标准转换都可以使用他,即static_cast可以把int转换为double,但不能把两个不相关的类对象进行转换,比如类A不能转换为一个不相关的类B类型。static_cast本质上是传统c语言强制转换的替代品。
reinterpret_cast操作符
该操作符用于将一种类型转换为另一种不同的类型,比如可以把一个整型转换为一个指针,或把一个指针转换为一个整型,因此使用该操作符的危险性较高,一般不应使用该操作符。
reinterpret_cast(重述转换)主要是将数据从一种类型的转换为另一种类型。所谓“通常为操作数的位模式提供较低层的重新解释”也就是说将数据以二进制存在形式的重新解释
String to int
#include <sstream> stringstream ss; int a=15454; ss<<a; string c=”str:”+ss.str(); cout<<c<<endl;
构造函数调用顺序
先调用 基类 构造函数
在调用 成员类 构造函数
最后调用 本身 的构造函数
析构顺序 相反
宏
1定义一个宏,输入两个参数,并返回期中较小的一个。 另外,当你写下面的代码时会发生什么事?
least = MIN(*p++, b);
解答:
#define MIN(A,B) ((A) <= (B) ? (A) : (B))
MIN(*p++, b)会产生宏的副作用
剖析:
这个面试题主要考查面试者对宏定义的使用,宏定义可以实现类似于函数的功能,但是它终归不是函数,而宏定义中括弧中的“参数”也不是真的参数,在宏展开的时候对“参数”进行的是一对一的替换。
程序员对宏定义的使用要非常小心,特别要注意两个问题:
(1)谨慎地将宏定义中的“参数”和整个宏用用括弧括起来。所以,严格地讲,下述解答:
1
2
#define MIN(A,B) (A) <= (B) ? (A) : (B)
#define MIN(A,B) (A <= B ? A : B )
都应判0分;
(2)防止宏的副作用。
宏定义#define MIN(A,B) ((A) <= (B) ? (A) : (B))对MIN(*p++, b)的作用结果是:
1
((*p++) <= (b) ? (*p++) : (b))
这个表达式会产生副作用,指针p会作2次++自增操作。
除此之外,另一个应该判0分的解答是:
1
#define MIN(A,B) ((A) <= (B) ? (A) : (B));
这个解答在宏定义的后面加“;”,显示编写者对宏的概念模糊不清,只能被无情地判0分并被面试官淘汰。
数组和指针
#include <iostream>
using namespace std;
void fun(int a[])
{
cout<<sizeof(a)<<endl;//4
*(a+1)=100;
cout<<*++a<<endl;//100
}
int main()
{
int a[10]={0,1,2,3,4,5,6,7,8,9};
cout<<sizeof(a)<<endl;
//cout<<*a<<*(++a)<<endl;//编译出错,a本身是常量指针
fun(a);
char *b="sfaasf";
char *c="123";
*b=”123”;//编译出错,b指向的内容是常量
b=c;
cout<<b<<endl;
return 0;
}
a 数组的 a 是 一个常量指针,不能修改,但a指向的内容可以修改,sizeof(a)等于数组大小*数组元素大小
当把a作为形参传入函数中时,a会退化为一个指针, a本身和指向的内容都可以修改 了,sizeof(a)就等于指针的大小
指针b所指向的内容是常量,不能修改,但b本身是个指针,可以修改;
多线程加锁
多线程调用时要进行保护时,主要是针对全局变量和静态变量的,函数内的局部变量不会受到影响
拷贝构造函数
三种调用拷贝构造函数的情况:
1. 需要用一个对象去初始化同一个类的另一个新对象;
2. 函数调用时,形参和实参的结合
3. 函数返回值为对象时
浅拷贝和深拷贝
浅拷贝是指在对象赋值时,只对对象中的数据成员进行简单的赋值,但对于存在动态成员(指针等),就会出现问题,使得两个对象的动态成员指向了同一个地址,而不是不同地址,内容相同。
自己写拷贝构造函数
class A{ public: A(){p =new int();} A(0){p=new int(0);} A(const A& a) { p= new int(); *p=*(a.p) } private: Int *p; }
初始化列表
(1)
class classA {...}; class classB { public: classB(classA a) {mA = a;} private: classA mA; }; (2) class classA {...}; class classB { public: classB(classA a): mA(a) {} private: classA mA; };
第二种比第一种效率更高
第一种方法,先调用a的默认构造函数,再调用operator=赋值函数;
第二种方法,直接调用拷贝构造函数,用初始化列表来进行构造,初始化列表的顺序只和变量申明的顺序有关
宏
宏只是预定义的函数,在编译阶段不进行类型安全性检查,在编译的时候将对应函数用宏命令替换。对程序性能无影响
p rintf ()
printf()的返回值是输出内容的大小
C onst
c onst int *a;//const在*a的前面,指的是*a为常量,不能变,即a指向的内容不能变
int const *a;//同上
int *const a;//const在a的前面,a为常量,即a这个指针为常量,a不能变
const int fun(const int a) const;
第一个const修饰返回值 第二个修饰参数第三个修饰调用对象
int getValue() const; //常成员函数
int getValue();
const关键字可以用于对重载函数的区分
常成员函数不能更新类的成员变量,也不能调用该类中没有用const修饰的成员函数,只能调用常成员函数
更正:常成员对象和一般成员对象都可以调用 常成员函数 。而一般成员函数只能被一般对象调用,常成员对象调用它会产生错误。
四种用法:
1. 修饰常量
2. 修饰函数参数 参数作为指针、引用传递时才有意义,值传递无意义
3. 修饰函数返回值
4. 修饰函数的定义体:任何不会修改数据成员的函数都应该声明为const类型。如果在编写const成员函数时,不慎修改了数据成员,或者调用了其它非const成员函数,编译器将指出错误,这无疑会提高程序的健壮性
接口
C++中的接口是指只包含纯虚函数的抽象类,不能被实例化。
一个类可以实现多个接口(多重继承)
(开)几次方
#include <math.h>
X 的y次方pow(x,y);
X 开y次方pow(x,1.0/y);
输入中有空格
当输入中有空格的时候,使用cin scanf()遇到空格会停止输入
要采用gets()或者 cin .getline(a, 10000 );第一个是要接受输入的指针,第二个是输入缓冲区大小
内存管理
简单理解:
1. 静态存储区 :内存在 程序编译的 时候就已经分配好,这块内存在程序的整个运行期间都存在。它主要存放静态数据、全局数据和常量。
2. 栈区 :在执行函数时, 函数 (包括main函数) 内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。(任何变量都处于站区,例如int a[] = {1, 2},变量a处于栈区。数组的内容也存在于栈区。)
3. 堆区 :亦称动态内存分配。程序在运行的时候用malloc或new申请任意大小的内存,程序员自己负责在适当的时候用free或delete释放内存。动态内存的生存期可以由我们决定,如果我们不释放内存,程序将在最后才释放掉动态内存。 但是,良好的编程习惯是:如果某动态内存不再使用,需要将其释放掉,并立即将指针置位NULL,防止产生野指针。
栈:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
堆:就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
自由存储区:就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
全局/静态存储区:全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
常量区:这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改
setw(8) swefill(*)
setw(int n)用来控制输出间隔
例如:
cout<<'s'<<setw(8)<<'a'<<endl;
则在屏幕显示
s a
//s与a之间有7个空格,setw()只对其后面紧跟的输出产生作用,如上例中,表示'a'共占8个位置,不足的用空格填充。若输入的内容超过setw()设置的长度,则按实际长度输出。
setw()默认填充的内容为空格,可以setfill()配合使用设置其他字符填充。
如
cout<<setfill('*')<<setw(5)<<'a'<<endl;
则输出:
****a //4个*和字符a共占5个位置
浮点数表示
F loat 8位阶码+符号位.+23位尾数
单精度浮点数有效位是7 10^7<2^(23+1)<10^8考虑第7位四舍五入,最少6位
阶码用移码表示:2^7+实际阶数的2进制表示
D ouble 11位阶码+符号位+52位尾数
双精度浮点数有效位是 16 10^16<2^(52+1)<10^17,考虑第16位四舍五入,最少15位
类的大小
Class A
{
Static int c;
Int c;
Void a();
Virtul void b();
};
类A的大小是 所有非静态成员变量大小之和+虚函数指针大小
文件操作
ofstream:写操作(输出)的文件类(由ostream引申而来)
ifstream:读操作(输入)的文件类(由istream引申而来)
fstream:可同时读写操作的文件类(由iostream引申而来)
#include <fiostream.h>
int main () {
ofstream examplefile ("example.txt");
if (examplefile.is_open()) {
examplefile << "This is a line.\n";
examplefile << "This is another line.\n";
examplefile.close();
}
return 0;
}
#include <iostream.h>
#include <fstream.h>
#include <stdlib.h>
int main () {
char buffer[256];
ifstream examplefile ("example.txt");
if (! examplefile.is_open())
{ cout << "Error opening file"; exit (1); }
while (! examplefile.eof() ) {
examplefile.getline (buffer,100);
cout << buffer << endl;
}
return 0;
}
类不声明成员变量访问修饰符默认private
结构体默认是public
友元函数 友元类
友元函数在类外部定义,在类中声明的可以访问类的隐藏成员的函数,不属于这个类
A为B的友元类,则A的所有成员函数为B的友元函数
class INTEGER { friend void Print(const INTEGER& obj);//声明友元函数 }; void Print(const INTEGER& obj){ //函数体} void main() { INTEGER obj; Print(obj);//直接调用}
模板
Template<class(typename) T>
//定义完一个模板T后立即使用,使用后
class Temp
{
public: Temp(T t){this.a=t}
private:T a;
}
模板的实例化
Temp<int> t1(3); Temp<string> t2(“abcd”); Template <class T1,class T2> //int i=0; 错误,模板定义和使用之间不得插入任何无关内容 T1 func( T1 a,T2 b) { cout<<b<<endl; return a; } int c=func(1,”abc”);
类的继承
P rivate属性不能继承
Private继承,父类public,protected 属性在子类中变为private
P rotected继承,父类public,protected属性变为protected
P ublic集成,父类public protected不改变
Private protected public 访问范围
P rivate:该类中的成员函数,该类的友元函数,其余都不能访问
P rotected:该类的成员函数,子类的成员函数,友元函数,不能被类的对象访问
P ublic:类中的成员函数,子类的函数,友元函数,类的对象都可以访问
注意:友元函数报告三种:一种普通的友元函数(非成员函数),设为友元的其他类的成员函数,设为友元类中的所有成员函数
C++多态性的实现
多态: 指当不同的对象收到相同的消息时,产生不同的动作
编译时多态:函数重载、运算符重载——静态绑定
运行时多态:虚函数机制——动态绑定
C++的多态性用一句话概括就是:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。
重写、重载、重定义
.重写(override):
父类与子类之间的多态性。子类重新定义父类中有相同名称和参数的虚函数。
1)被重写的函数不能是static的。必须是virtual的(即函数在最原始的基类中被声明为virtual )。
2)重写函数必须有相同的类型,名称和参数列表(即相同的函数原型)
3)重写函数的访问修饰符可以不同。尽管virtual是private的,派生类中重写改写为public,protected也是可以的
2.重载(overload):
指函数名相同,但是它的参数表列个数或顺序,类型不同。但是不能靠返回类型来判断。
3.重定义(redefining):
子类重新定义父类中有相同名称的非虚函数(参数列表可以不同)。
重写与重载的区别 (override) PK (overload)
1、方法的重写是子类和父类之间的关系,是垂直关系;方法的重载是同一个类中方法之间的关 系,是水平关系。
2、重写要求参数列表相同;重载要求参数列表不同。
3、重写关系中,调用那个方法体,是根据对象的类型(对象对应存储空间类型)来决定;重载关系,是根据调用时的实参表与形参表来选择方法体的。
Volatile
修饰符,修饰后的变量可以防止被编译优化,每次取值时会逐语句取值(多线程)
STL类库编程
各种容器的优缺点及底层实现
s String
#include <string>
string a,b;
cin>>a>>b;
a.insert(a.length(),b);
a[0]
VECTOR向量
#include <vector> Vector<int> a,b; a.push_back(3); a.erase()
QUEUE队列
#include <queue> queue<int> a; a.push(1); a.push(2); cout<<a.front()<<a.back(); a.pop(); a.size(); a.empty();
priority_queue 优先队列
STACK栈
#include <stack> stack<int> a; a.push(1); cout<<a.top();- a.pop(1); a.empty(); a.size();
LIST双向链表
将元素按顺序储存在链表中.与 向量(vectors)相比,它允许快速的插入和删除,但是随机访问却比较慢
#include <list>
list<int> l ,l2 ;
l.assign(8,1); //将l变为8个1(清空掉原来的内容,重新赋值8个1)
l2.assign(l.begin(),l.end());把l的内容赋给l2
l.front();访问第一个元素
l.back();访问最后一个元素
l.push_back(5);在链表尾部插入新元素
l.push_front(6);在链表首部插入新元素
l.pop_back();
i.pop_front();
l.begin()返回头元素的迭代器
l.end();返回尾元素的迭代器
l.rbegin();返回头元素的反向迭代器(双向链表)
l.rend();返回尾元素的反向迭代器
l.insert(l.begin()+5,1);制定位置插入
l.erase(l.begin()+4)删除指定位置
l.merge(l2) l与l2归并(合并后排序)升序
l.splice(l.end(),l2);把l2插入到l后面
swap(l,l2);交换两个链表
l.sort(comp);用自定义比较函数comp给l排序
bool comp(const int &a,const int &b) { return a>b; } l.reverse(); 反转 l.unique(); 删除重复元素 l.size(); l.clear(); l.empty(); unique
MAP映射
Map<key,value>
#include <map>
#include <algorithm>
map<string,int> m;
//插入新元素
m.insert(pair<string,int>(“aaa”,1)));
m.insert(map<string,int>::value_type(“bbb”,2));
m[“ccc”]=3;
map<string,int>::iterator it;
it=m.find(“aaa”);
if(it==m.end())
cout<<”can’t find”;
m.erase(it);删除key为aaa的元素
MAP自动按照key值升序排列,不能对它进行排序
m.upper_bound(“bbb”);//返回键值大于”bbb”的第一个元素的位置
swap();
底层实现红黑树
SET 集合
实现了红黑树的平衡二叉检索树的数据结构
入元素时,它会自动调整二叉树的排列,把元素放到适当的位置,以保证每个子树根节点键值大于左子树所有节点的键值,小于右子树所有节点的键值;另外,还得保证根节点左子树的高度与右子树高度相等。
平衡二叉检索树使用中序遍历算法,检索效率高于vector、deque和list等容器,另外使用中序遍历可将键值按照从小到大遍历出来。
构造set集合主要目的是为了快速检索,不可直接去修改键值。
多线程开发
哈希冲突及解决
哈希存储:牺牲空间存储换来时间上的效率
如有1000个数据,但我们开10000的空间来存储,对数据的key值得到hash值存到对应的位置,这样在查找时,就能根据key值很快的找到某个数据。
但是有时候对于数据的key值范围不能确定,所以多个不同的key得到的hash值可能相同,这时就产生了哈希冲突,
对于常见的取余数得到hash值的方法,可以采用开放地址法来处理hash冲突,hash=(hash(key)+d)%n给冲突的hash值继续加一个增量d,继续取余数,直到找到一个不冲突的位置为止
补码的好处
原码在做加减运算时十分复杂,如8-2就等于8+(-2),而-2在计算机中就是用2的补码表示的,直接用8的原码加上2的补码
数据结构
基本数据类型
32位操作系统
int 4字节
long 4字节
char 1字节
*指针 4字节
f loat 4字节
S hort int 2字节
l ong long int 8字节
double 8字节
64位操作系统
long 8字节
* 8字节
结构体:所有成员加起来,有内存对齐;
联合:成员最大长度,有内存对齐
枚举:占一个int大小,枚举成员的默认值等
数组
char *a和char a[]的区别
char*a ,a这个指针作为变量是存储在栈上的,可以修改,但指针指向的内容是存放在常量区,无法修改
c har *a= ” fasfsa ” ;
char *b= ” gasgsag ” ;
a=b;//正确
*a=*b;//错误
char a[] a这个指针变量和数组中的内容都是存储在栈上的, 可以修改
结构体
typedef struct myStruct { int val; string a; myStruct(int val,string a) { this->val=val; this->a=a; } }MyStruct; MyStruct b(5,”sfafasf”); 结构体对齐,sizeof()占多少字节 struct a{ struct b{ struct c{ int a; int a; long long int a; int b; char b; int b; char c; int c; long long int c; short int d; short int d; char c; };共占12字节 }; 16字节 } 4*8=32字节
链表
反转、找到中间元素等
树
二叉树
霍夫曼树
带权路径最小的最优树
二叉、红黑等树,遍历
图
队列
堆
栈
算法
排序
排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。
插入排序
直接插入
默认最前面的是已经排好序的,然后依次把后面的插入到合适的位置。
适用于数据量较小
时间复杂度:O(n^2)
最佳复杂度:O(n)排好序的时候
最差:O(n^2)倒序
void insertSort(int a[])
{
If(a.size()<2)
return;
for(int i = 1;i<a.size();i++)
{
int key =a[i];
int j = i-1;
while(j>0&&a[j]>key)
{
a[j+1]=a[j];
j--;
}
a[j+1]=key;
}
}
希尔排序
时间复杂度:O(n^1.3)
先将要排序的一组记录按某个增量d(n/2,n为要排序数的个数)分成若干组子序列,每组中记录的下标相差d.对每组中全部元素进行直接插入排序,然后再用一个较小的增量(d/2)对它进行分组,在每组中再进行直接插入排序。继续不断缩小增量直至为1,最后使用直接插入排序完成排序。
void ShellInsertSort(int a[], int n, int dk)
{
if(n<2)
return;
for(int i=dk; i<n ; i++)
{
if(a[i-dk] > a[i])
{
int key = a[i];
int j = i - dk;
while(j>=0 && a[j] > key)
{
a[j+dk] = a[j];
j -= dk;
}
a[j+dk] = key;
}
}
}
void ShellSort(int a[], int n)
{
int dk = n/2;
while(dk>=1)
{
ShellInsertSort(a, n, dk);
dk /=2;
}
}
选择排序
简单选择排序
每次选择一个最大值放到最后(最小值放到最前面)
时间复杂度:O(n^2)
void selectSort(int a[]) { If(a.size()<2) return; for(int i=0;i<a.size()-1;i++) { int min= i; for(int j=i+1;j<a.size();j++) { If(a[j]<a[min]) min=j; } swap(a[i],a[min]); } }
改进每趟选出当前最大和最小,最多只需要N/2趟排序
堆排序
时间复杂度:O(nlogn)
建立堆的复杂度是O(n),只建立一次
调整堆的时间复杂度是o(logn),调用n-1次 所以是nlogn
不稳定
//array是待调整的堆数组,i是待调整的数组元素的位置,nlength是数组的长度
//本函数功能是:根据数组array构建大根堆
void HeapAdjust(int array[],int i,int nLength)
{
int nChild;
int nTemp;
for(;2*i+1<nLength;i=nChild)
{
//子结点的位置=2*(父结点位置)+1
nChild=2*i+1;
//得到子结点中较大的结点
if(nChild<nLength-1&&array[nChild+1]>array[nChild])++nChild;
//如果较大的子结点大于父结点那么把较大的子结点往上移动,替换它的父结点
if(array[i]<array[nChild])
{
nTemp=array[i];
array[i]=array[nChild];
array[nChild]=nTemp;
}
else break; //否则退出循环
}
}
//堆排序算法
void HeapSort(int array[],int length)
{
int i;
//调整序列的前半部分元素,调整完之后第一个元素是序列的最大的元素
//length/2-1是最后一个非叶节点,此处"/"为整除
for(i=length/2-1;i>=0;--i)
HeapAdjust(array,i,length);
//从最后一个元素开始对序列进行调整,不断的缩小调整的范围直到第一个元素
for(i=length-1;i>0;--i)
{
//把第一个元素和当前的最后一个元素交换,
//保证当前的最后一个位置的元素都是在现在的这个序列之中最大的
交换arry[i] arry[0]
//不断缩小调整heap的范围,每一次调整完毕保证第一个元素是当前序列的最大值
HeapAdjust(array,0,i);
}
}
交换排序
冒泡
通过两两交换,每次找出剩下元素中的最大值(最小值)
时间复杂度:O(n^2)
Void bubbleSort(int a[]) { If(a.size()<2) return; for(int i=0;i<a.size();i++) { for(int j=0;j<a.size()-i-1;j++) { If(a[j]>a[j+1]) Swap(a[j],a[j+1]); } } }
改进冒泡1
.设置一标志性变量 pos, 用于记录每趟排序中最后一次进行交换的位置。由于 pos 位置之后的记录均已交换到位 , 故在进行下一趟排序时只要扫描到 pos 位置即可
1. void Bubble_1 ( int r[], int n) {
2. int i= n -1; //初始时,最后位置保持不变
3. while ( i> 0) {
4. int pos= 0; //每趟开始时,无记录交换
5. for ( int j= 0; j< i; j++)
6. if (r[j]> r[j+1]) {
7. pos= j; //记录交换的位置
8. int tmp = r[j]; r[j]=r[j+1];r[j+1]=tmp;
9. }
10. i= pos; //为下一趟排序作准备
11. }
12. }
改进冒泡2
统冒泡排序中每一趟排序操作只能找到一个最大值或最小值 , 我们考虑利用在每趟排序中进行正向和反向两遍冒泡的方法一次可以得到两个最终值 ( 最大者和最小者 ) , 从而使排序趟数几乎减少了一半。
1. void Bubble_2 ( int r[], int n){
2. int low = 0;
3. int high= n -1; //设置变量的初始值
4. int tmp,j;
5. while (low < high) {
6. for (j= low; j< high; ++j) //正向冒泡,找到最大者
7. if (r[j]> r[j+1]) {
8. tmp = r[j]; r[j]=r[j+1];r[j+1]=tmp;
9. }
10. --high; //修改high值, 前移一位
11. for ( j=high; j>low; --j) //反向冒泡,找到最小者
12. if (r[j]<r[j-1]) {
13. tmp = r[j]; r[j]=r[j-1];r[j-1]=tmp;
14. }
15. ++low; //修改low值,后移一位
16. }
17. }
快速排序
时间复杂度:O(nlogn)
空间复杂度:O(nlogn)每一趟快排过后需要花费o(n)来存储当前状态,再递归进入下一趟快排,共需logn趟,所以空间复杂度nlogn
不稳定
1. void swap( int *a, int *b)
2. {
3. int tmp = *a;
4. *a = *b;
5. *b = tmp;
6. }
7.
8. int partition( int a[], int low, int high)
9. {
10. int privotKey = a[low]; //基准元素
11. while (low < high){ //从表的两端交替地向中间扫描
12. while (low < high && a[high] >= privotKey) --high; //从high 所指位置向前搜索,至多到low+1 位置。将比基准元素小的交换到低端
13. swap(&a[low], &a[high]);
14. while (low < high && a[low] <= privotKey ) ++low;
15. swap(&a[low], &a[high]);
16. }
17. print(a,10);
18. return low;
19. }
20.
21.
22. void quickSort( int a[], int low, int high){
23. if (low < high){
24. int privotLoc = partition(a, low, high); //将表一分为二
25. quickSort(a, low, privotLoc -1); //递归对低子表递归排序
26. quickSort(a, privotLoc + 1, high); //递归对高子表递归排序
27. }
28. }
快速排序的改进
对于长度大于8的用快排,小于8的用插入排序
归并排序
时间复杂度:O(nlogn)
稳定!
将两个顺序序列合并成一个顺序序列的方法
第一步:申请空间,使其大小为两个已经 排序 序列之和,该空间用来存放合并后的序列
第二步:设定两个 指针 ,最初位置分别为两个已经排序序列的起始位置
第三步:比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
重复步骤3直到某一指针超出序列尾
将另一序列剩下的所有元素直接复制到合并序列尾
1. void Merge(ElemType *r,ElemType *rf, int i, int m, int n)
2. {
3. int j,k;
4. for (j=m+1,k=i; i<=m && j <=n ; ++k){
5. if (r[j] < r[i]) rf[k] = r[j++];
6. else rf[k] = r[i++];
7. }
8. while (i <= m) rf[k++] = r[i++];
9. while (j <= n) rf[k++] = r[j++];
10. }
桶排序 / 基数排序
时间复杂度:线性接近O(n)
关键码排序,扑克牌按花色、大小排序
分治
分治法是一个 递归 地求解问题的过程
分治法在每一层递归上有三个步骤:
1. 分解:通过某种方法,将原问题分解成若干个规模较小, 相互独立 ,与原问题形式相同的子问题
2. 解决:若子问题规模小到可以直接解决(称为基本问题),则直接解决,否则把子问题进一步分解,递归求解
3. 合并:通过某种方法,将子问题的解合并为原问题的解
二分法
给一个二叉树和一个值,判断是否存在一条由根到叶子的路径使得路径节点之和等于给定值
递归
搜索 BFS DFS
BFS入队
DFS递归,入栈
模拟
贪心
动规
回溯
设计模式
OO设计的原则
策略模式
生产者消费者
1. 解耦,当生产者的生产函数改变时,消费者不会受到影响
2. 支持并发,解决生成消费速度不匹配等问题,不会阻塞
观察者模式
简单工厂、工厂方法、抽象工厂
MVC
单例
singleton单例模式,简单说就是只有自己,为自己设计。降低了重复使用。降低资源使用率
flyweight设计中的享元模式,避免大量拥有相同内容的小类的开销,因为他让大家共享一个类
LINUX
错题点
权限命令
# 改变权限
chmod 777 filepath 指定文件 filepath 为所有用户可读,可写,可执行
读写执行分别对应数字1 2 4,加起来就是7
# 改变所有者
chown test filepath
改变 filepath 的所有者为test
# 改变所属组
chgrp user filepath
改变 filepath 的所属组为user
常用命令
ls cd pwd cp chmod chwon等