C/C++ 题库
大约 55 分钟
C/C++ 题库
1.变量的声明和定义的区别
变量定义: 为变量分配存储空间,还可以为变量指定初始值。程序中,变量有且只有一个定义
变量声明: 用于向程序表明变量的类型和名字。定义也是声明,当定义变量的时候我们声明了他的类型和名字。
可以通过 extern 声明变量名而不定义它。extern 声明不是定义,它不分配存储空间。它只是告诉我们
变量的定义在其他地方。程序中变量可以声明多次,但只能定义一次。
2.写出 bool、int、float、指针变量与 “零值”的比较的 if 语句
if (flag)
A;
else
B;
if (0 != flag)
A;
else
B;
if (NULL != flag)
A;
else
B;
if (flag >= -NORM && flag <= NORM)
A;
else
B;
/*特殊说明:float的表示方法的问题,float本身就是不精确的,精确度在小数点,float是 符号位+8bit的指数为+23bit的小数位组成的。那么这样就会有一个问题,如果数据中的
小数位超过了用来表示小数位的bit长度,就会有数据丢失,这个时候通常计算机会按照一定的规律进行转换得到一个非常接近的数值,例如13.765432有时候得到的值却是13.7654319。
这就是所谓的float类型不精确的原因。*/
3.sizeof 和 strlen 的区别
sizeof: 是操作符 在编译的时候就计算出结果 参数可以是数据类型,也可以是变量 计算的是内存大小
strlen: 是库函数 在程序运行的时候才计算出结果 参数是以'\0'结尾的字符串 计算的是字符串长度
4.static 关键字作用
C语言 static 关键字作用:
1.修饰局部变量,相当于全局变量,延长了生命周期,直到程序运行结束后才释放。
2.修饰全局变量,这个静态全局变量就只能在本文件中被访问,不能被其他文件访问,即使是 extern 外部声明也不行(全局变量可以被外部文件问)。
3.修饰函数,这个函数就只能在本文件中被调用,外部文件不能被访问调用,外部文件的函数可以同名。
static 修饰的局部变量存放在内存中的全局静态存储区
C++ static 关键字作用:
1.修饰成员函数,静态成员之间可以相互访问,包括静态成员函数访问静态成员数据和访问静态成员函数。不能访问非静态成员函数和非静态成员数据。
调用方法 类对象.静态成员函数 或者是类对象::静态成员函数。
静态成员函数只属于类本身,随着类的加载而存在,不属于任何对象,是独立存在的。
2.修饰成员数据,存储在全局静态区,静态数据成员定义时需要分配内存,所以无法在类声明中定义。不在类的声明中定义,是因为在类的声明中定义会
导致每次类的初始化都包含该静态成员数据,与设计不符合。所有该类的实体对象(instance)都共用一个静态成员数据。
3.protected 和 privated 修饰的静态函数无法被类外访问
5.malloc free 和 new delete 区别
C语言
malloc、free 是库函数 需要头文件支持 只是申请内存和释放内存,无法对自定义类型对象进行构造和析构
malloc 申请需要给定内存大小 成功返回 void * 需要强制转换 malloc 失败返回NULL
C++
new delete 是关键字 需要编译器支持 new 会先申请足够的内存(底层用malloc 实现),然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针
delete 会先调用类型的析构函数,然后释放内存(底层是用free 实现)
new 申请无需给定内存大小 编译器会自行计算 成功返回 对象类型指针 无需强制转换 new 失败会抛出异常
6.写一个标准宏 MIN
#define MIN(a, b) ((a) <= (b) ? (a) : (b))
//要注意这个情况
int b = 2; //b = 1
int * p = NULL;
p = (int *) malloc(sizeof(int));
*p = 1;
qDebug() << MIN(++*p, b); // p = 3 b = 2; p的值变化了2次
// p = 2 b = 1; p的值变化了1次
7.一个指针可以是 volatile 吗
可以的,指针和普通变量一样,有时也有变化程序的不可控性。防止编译优化,每次编译都重新读值
8.a 和 &a 的区别
int A[5] = {1,2,3,4,5};
int *P = (int *) (&A + 1); //P 的地址 :指向数组A的首地址 + sizeof(int) * 5
qDebug() << A; // 0x44e9aff8d0
qDebug() << P; // 0x44e9aff8e4
qDebug() << &A[0]; // 0x44e9aff8d0
qDebug() << &A[4]; // 0x44e9aff8e0
qDebug() << &A[5]; // 0x44e9aff8e4
printf("%d -- %d\n", *(A + 1), *(P - 1)); // 答案 -- 5
// 思考 int *P = (int *) (A + 1); 时 打印printf("%d -- %d\n", *(A + 1), *(P - 1));是多少 2 -- 1
9./C++ 内存分配
C语言 内存四区
堆区 手动分配和释放 malloc/free
栈区 系统自动分配和释放,存放局部变量和函数参数
全局区 程序运行时分配的内存
细分:常量区 未初始化的全局数据区 初始化的全局数据区 静态变量也存放于此
代码区 二进制代码存放的地方
C++ 内存5区
堆区 动态申请内存,由程序员自己控制
栈区 存放函数局部变量、函数参数、返回地址等,系统自动分配
全局区/静态区 (.bss段和.data段) 存放全局变量和静态变量,程序结束时系统自动释放,
常量存储区 (.data段) 存放常量
代码区 (.text段) 存放二进制代码
堆和栈区别
栈 自动分配和释放 速度快 效率高但无法控制 内存地址连续 系统设定好的最大容量, 向低地址扩展
堆 手动分配和释放 速度慢 效率低但能控制,容易产生碎片 内存地址不连续 系统用链表来存储空闲内存地址,大小受限于系统的有效虚拟内存 向高地址扩展
10.strcpy sprintf 和 memcpy 区别
1. 操作对象不同:strcpy 是字符串,memcpy 可以是任意数据类型 sprintf 目的对象是字符串,但源对象可以任意基本数据类型
2. 执行效率,memcpy 最高, strcpy 次之, sprintf 效率最低
3. strcpy : 字符串拷贝,不需要指定长度,它遇到 '\0' 结束符自动结束
memcpy : 任意数据类型的拷贝,要指定长度,不会遇到 '\0' 结束符就自动结束
sprintf: 用于拼接字符串和其他基本数据类型
11.设置地址为0x67a9的整形变量的值为0xaa66
int *ptr;
ptr = (int *)0x67a9;
*ptr = 0xaa66;
12.面向对象的三大特征
1.封装,使代码模块化。
2.继承,扩展已存在的代码模块,目的是实现了代码重用。(虚继承)
3.多态,实现接口重用。 实现方式细分为:重载和重写 重载:存在多个同名函数但是函数参数的个数和类型不同 重写:子类重新定义父类的虚函数方法
补充:析构函数为虚函数
1.析构函数定义为虚函数时,基类指针指向派生类对象, 如果删除该指针(delete p;), 就会调用该指针指向的派生类析构函数,而派生类的析构函数又会自动调用
基类的析构函数,这样整个派生类对象就完全释放了。
2.析构函数不定义为虚函数时,基类指针指向派生类对象,如果删除该指针(delete p;), 就只会调用该指针指向的基类析构函数,而不调用派生类的析构函数,这样就会造成派生类对象析构不完整。
3.派生类指针操作派生类对象,基类析构函数不是虚函数:会先释放派生类资源,再释放基类资源,不会出现资源泄露。
举例:派生类指针操作派生类对象,基类析构函数不是虚函数
#include<iostream>
using namespace std;
class ClxBase{
public:
ClxBase() {};
~ClxBase() { cout << "Output from the destructor of class ClxBase!" << endl; };
void DoSomething() { cout << "Do something in class ClxBase!" << endl; };
};
class ClxDerived : public ClxBase{
public:
ClxDerived() {};
~ClxDerived() { cout << "Output from the destructor of class ClxDerived!" << endl; };
void DoSomething() { cout << "Do something in class ClxDerived!" << endl; };
};
int main(){
ClxDerived *p = new ClxDerived; // 派生类指针操作派生类对象
p->DoSomething();
delete p;
system("pause");
return 0;
}
//Do something in class ClxDerived!
//Output from the destructor of class ClxDerived
//Do something in class ClxBase
派生类指针操作派生类对象,基类析构函数不是虚函数,此时会先调用派生类析构函数,释放派生类资源,然后再调用基类析构函数,释放基类资源,不会出现资源泄露情况。
举例:基类指针操作派生类对象,基类析构函数不是虚函数
#include<iostream>
using namespace std;
class ClxBase{
public:
ClxBase() {};
~ClxBase() { cout << "Output from the destructor of class ClxBase!" << endl; };
void DoSomething() { cout << "Do something in class ClxBase!" << endl; };
};
class ClxDerived : public ClxBase{
public:
ClxDerived() {};
~ClxDerived() { cout << "Output from the destructor of class ClxDerived!" << endl; };
void DoSomething() { cout << "Do something in class ClxDerived!" << endl; };
};
int main(){
ClxBase *p = new ClxDerived; // 基类指针操作派生类对象
p->DoSomething();
delete p;
system("pause");
return 0;
}
//Do something in class ClxBase!
//Output from the destructor of class ClxBase!
基类指针操作派生类对象,基类析构函数不是虚函数;此时只调用了基类的析构函数,释放了基类的资源,没有调用派生类的析构函数,没有释放派生类资源。调用的DoSomething()也是基类定义的函数,不是派生类的。这样的只能删除基类资源,不能删除派生类资源,造成了内存泄漏。
举例:基类指针操作派生类对象,基类析构函数是虚函数
#include<iostream>
using namespace std;
class ClxBase{
public:
ClxBase() {};
// 定义为虚函数
virtual ~ClxBase() { cout << "Output from the destructor of class ClxBase!" << endl; };
virtual void DoSomething() { cout << "Do something in class ClxBase!" << endl; };
};
class ClxDerived : public ClxBase{
public:
ClxDerived() {};
~ClxDerived() { cout << "Output from the destructor of class ClxDerived!" << endl; };
void DoSomething() { cout << "Do something in class ClxDerived!" << endl; };
};
int main(){
ClxBase *p = new ClxDerived; // 基类指针操作派生类对象
p->DoSomething();
delete p;
system("pause");
return 0;
}
//Do something in class ClxDerived!
//Output from the destructor of class ClxDerived!
//Output from the destructor of class ClxBase!
基类指针操作派生类对象,基类析构函数是虚函数。释放资源时,会先调用派生类析构函数,释放派生类资源,然后在调用基类析构函数,释放基类资源。因为DoSomething()函数在基类中定位为虚函数,所以执行的时候也是调用的派生类的DoSomething()函数。
基类析构函数定义为虚函数的情况说明:如果不需要基类对派生类及其对象进行操作,则不要把基类析构函数定义为虚函数,因为这样会增加开销,当类里面有定义虚
函数时,编译器会给类添加一个虚函数表,里面存放虚函数指针,这样会增加类的存储空间。所以,只有一个类被当做基类时,且有使用到基类指针来操作派生类的情况时,才会把基类的析构函数写成虚函数。
纯虚函数:是只有声明没有实现的虚函数。包含纯虚函数的类不能定义其对象。像这种只能用于被继承而不能直接创建对象的类被称为抽象类,抽象类是无法定义抽象类对象的。
virtual void func()=0;//这便是声明了一个纯虚函数 也就是在虚函数尾部加上" =0 " 一个虚函数便被声明成为了一个纯虚函数
// 等于0表示该函数仅声明而没有函数体
纯虚函数和虚函数的区别:
1.纯虚函数只有定义没有实现,虚函数既有定义也有实现
2.包含纯虚函数的类不能定义对象,而包含虚函数的可以。
如果子类想要实例化,纯虚函数必须被重写,如果派生类继承了包含纯虚函数的基类,却没有重纯虚函数。那么相当于子类也继承了纯虚函数,子类也变成了抽象类,不能被实例化。
13.C++ 空类有哪些成员函数
6个
缺省构造函数
拷贝构造函数属
析构函数
赋值运算符
取值运算符
取值运算符 const
class Empty{
public:
Empty(); // 缺省构造函数
Empty( const Empty& ); // 拷贝构造函数
~Empty(); // 析构函数
Empty& operator=( const Empty& ); // 赋值运算符
Empty* operator&(); // 取址运算符
const Empty* operator&() const; // 取址运算符 const
};
1.缺省构造函数
一种特殊的成员函数,当创建一个类对象时,调用构造函数对类的数据成员进行分配内存和初始化。
构造函数的命名和类名完全相同。
构造函数可以重载,可以多个,带参数。
2.缺省拷贝构造函数
函数名和类名一样,有两种原型
Empty(Empty & a);
Empty(const Empty & a);
参数为地址参数,为了防止无线构造,形成死循环
const 的目的是常引用,不能改变里面的值
为什么拷贝构造函数的参数必须是对象的引用而不是直接传值?
因为通过传值的方式将实参传递给形参,这个中间本身就要经历一次对象的拷贝过程,而对象的拷贝又需要调用拷贝构造函数,如此一来就会进入死循环,无解,所以必须是对象的引用。
拷贝构造函数除了能用对象的引用这样的参数外,还能有其他参数,但这个参数必须给出默认值。
Empty(const Empty &e, int a = 5);
如果声明拷贝构造函数,系统则会自动为类生成一个拷贝构造函数,但是他的功能非常简单,只能将原有对象的所有成员变量复制给当前创建的对象。
举例:
#include<iostream>
using namespace std;
class Array
{
public:
Array(){length = 0;num = NULL;}
Array(int *A,int n);
void setnum(int vallue,int index);
int *getaddress();
int getaddress();
void display();
private:
int length;
int *num;
};
Array::Array(int *A,int n)
{
num = new int [n];
length = n;
for (int i = 0;i < n; i++)
num[i] = A[i];
}
void Array::setnum(int value,int index)
{
if(index < length)
num[index] = value;
else
cout<<"index out of range!"<<endl;
}
void Array::display()
{
for(int i = 0;i < length;i++)
cout<<num[i]<<" ";
cout<<endl;
}
int *Arry::getaddress()
{
return num;
}
int main()
{
int A[5] = {1,2,3,4,5};
Array arr1(A,5);
arr1.display();
Array arr2(arr1);
arr2.display();
arr2.setnum(8,2);
arr2.display();
arr1.display();
cout<<arr1.getaddress()<<" "<<arr2.getaddress()<<endl;
return 0;
}
运行结果如下:
1 2 3 4 5
1 2 3 4 5
1 2 8 4 5
1 2 8 4 5
00331F58 00331F58
/*
在本例中,我们重新定义了一个Array类,可以理解为一个整形数组类,这个类中我们定义了两个成员变量:整形指针num和数组长度length。
类中定义了一个默认构造函数,声明了一个带参构造函数。默认构造函数很简单,带参构造函数则是用于将一个已有的数组全部拷贝给类对象。
除了两个构造函数之外,我们还定义四个成员函数,一个是用于修改数组中数值的setnum函数、一个打印数组中所有元素的display函数、一个返回数组首地址的函数getaddress和一个返回数组长度的函数getlength。除了默认构造函数之外和getlength函数之外,所有的函数在类外都有定义。
接下来我们看一下主函数。主函数中,我们先定义了一个数组,包含五个元素,分别是从1到5。之后用Array类创建对象arr1,并且用A数组初始化对象arr1,此时arr1对象相当于拥有一个数组,该数组包含5个元素,打印出来的结果是“1 2 3 4 5 ”,没有问题。之后用arr1对象初始化arr2对象,因为我们在类中没有显示地定义一个拷贝构造函数,因此系统会自动为我们生成一个拷贝构造函数,该拷贝构造函数的定义如下:
Array::Array(Array &a)
{
length = a.length;
num = a.num;
}
通过系统自动生成的拷贝构造函数完成arr2对象的创建,同样的arr2也是有5个元素的数组,打印出来的结果是“1 2 3 4 5 ”,同样没有问题。
之后我们调用成员函数setnum,将arr2对象下标为2的元素修改为8(原先是3)。此时打印arr2中数组元素,结果为“1 2 8 4 5 ”,正确,arr2第三个元素确实被修改掉了。
后我们再调用arr1.display(),奇怪的事情发生了,它的打印结果竟然也是“1 2 8 4 5 ”!我们之前并未修改过第三个元素的值的,这是怎么一回事呢?不急,我们再来看一下最后一句“cout<<arr1.getaddress()<<" "<<arr2.getaddress()<<endl;”其显示结果竟然是一样的!看到这里是不是有些明白了上面的问题呢?很明显,arr1和arr2所指向的数组是同一个数组,在内存中的位置是一致的,因此当我们利用对象arr2去修改数组中第三个元素的数值的时候,arr1中的数组也被修改了,其实它们本来就是使用的是同一个内存中的数组而已.
*/
拷贝构造函数的参数为引用,系统自动生成的拷贝构造函数功能简单,只是将arr1 的数组首地址直接赋值给arr2的数值首地址,arr1 和 arr2 的数据成员 num 其实是同一个内存,同一个数组。 要避免这种情况,就要自己加上一个拷贝构造函数。
例子:
#include<iostream>
using namespace std;
class Array
{
public:
Array(){length = 0;num = NULL;}
Array(int *A,int n);
Array(Array &a);
void setnum(int value,int index);
int *getaddress();
void display();
int getlength(){return length;}
private:
int length;
int *num;
};
Array::Array(Array&a)
{
if(a.num!=NULL)
{
length = a.length;
num = new int [length];
for(int i = 0;i < length;i++)
num[i] = a.num[i];
}
else
{
length = 0;
num = 0;
}
}
Array::Array(int *A,int n)
{
num = new int [n];
length = n;
for(int i = 0;i < n;i++)
num[i] = A[i];
}
void Array::setnum(int value,int index)
{
if(index <length)
num[index] = value;
else
cout<<"index out of range!"<<endl;
}
void Array::display()
{
for (int i = 0;i < length;i++)
cout<<num[i]<<" ";
cout<<endl;
}
int *Array::getaddress()
{
return num;
}
int main()
{
int A[5] = {1,2,3,4,5};
Array arr1(A,5);
arr1.display();
Arry arr2(arr1);
arr2.display();
arr2.setnum(8,2);
arr2.display();
arr1.display();
cout<<arr1.getaddress();" "<<arr2.getaddress()<<endl;
return 0;
}
运行结果如下:
1 2 3 4 5
1 2 3 4 5
1 2 8 4 5
1 2 3 4 5
00311F58 00487268
3.缺省析构函数
析构函数只有一个,不能重载
析构函数不能有参数
在主函数中,析构函数在 return 语句之前执行
4.缺省赋值运算符
赋值运算符是对一个已经初始化的对象进行赋值操作
如果不想写拷贝构造函数和赋值函数,又不允许别人使用编译器生成的缺省函数,最简单的办法是将拷贝构造函数和赋值声明为私有函数。
5.缺省取值运算符
6.缺省取值运算符 const
#include <iostream>
using namespace std;
class A
{
public:
A()
{
cout<<"构造函数"<<endl;
}
~A()
{
cout<<"希构函数"<<endl;
}
A(const A &)
{
cout<<"拷贝构造函数"<<endl;
}
A& operator=(const A &)
{
cout<<"赋值运算"<<endl;
}
A* operator&()
{
cout<<"取地址"<<endl;
}
const A* operator&()const
{
cout<<"const取地址"<<endl;
}
};
int main(int argc, char **argv)
{
A c1; //构造
A c2 = c1; //拷贝构造
c2 = c1; //赋值运算
A *pa = &c1;//取地址
const A c3; //构造
const A * pb = &c3;//const 取地址
return 0;
}
14.谈谈拷贝构造函数和赋值运算符的认识
1.拷贝构造函数生成新的对象,而赋值运算符不是
2.由于拷贝构造函数是直接构造一个新的类对象, 所以在初始化这个对象之前不需要检查原对象和新对象是否相同,而赋值运算符需要这个操作,另外赋值运算中如果原来的对象有内存分配需要先把内存释放掉
3.在类中有指针类型成员变量时,一定要重写拷贝构造函数和赋值操作符,不能使用默认的,原因是因为不重写的话,是浅拷贝,当一个对象释放后,另外一个对象在释放就报错了。
15.用 C++设计一个不能被继承的类
class A :
{
private:
A();
}
class B : public A
{
}
B 就不能继承A 因为基类A的构造函数是私有的 无法构造,构造函数的顺序是先基类构造函数,然后是成员函数,然后是派生类
要是构造函数是私有的,也不能创建对象了,只有通过特殊方法,在基类中写一个静态成员函数来创建对象
例子:
#include <iostream>
using namespace std;
class A{
private:
A(){
;
}
public:
static A* get(){
A* a1 = new A();
return a1;
}
~A(){
;
}
};
int main(){
A b; //错误
A* a = A::get(); //正确
return 0;
}
A
/ \
B C
\ /
D
菱形继承:即两个派生类继承同一个基类,同时两个派生类又作为基本继承给同一个派生类。这种继承形如菱形,故又称为菱形继承。
菱形继承举例: D 多继承 B C,而 B继承A,C又继承A
菱形继承,菱形继承的问题,数据冗余和二义性
class Person//人类
{
public :
string _name ; // 姓名
};
class Student : public Person//学生类
{
protected :
int _num ; //学号
};
class Teacher : public Person//老师类
{
protected :
int _id ; // 职工编号
};
class Assistant : public Student, public Teacher//助理类
{
protected :
string _majorCourse ; // 主修课程
};
void Test ()
{
// 这样会有二义性无法明确知道访问的是哪一个
Assistant a ;
a._name = "peter";
// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
}
要解决这个问题,要用菱形虚拟继承
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。
class Person
{
public :
string _name ; // 姓名
};
class Student : virtual public Person
{
protected :
int _num ; //学号
};
class Teacher : virtual public Person
{
protected :
int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
string _majorCourse ; // 主修课程
};
void Test ()
{
Assistant a ;
a._name = "peter";
}
继承和组合的区别?什么时候用继承?什么时候用组合?
public继承 是一种is-a的关系。就是说每个派生类对象都是一个基类对象
组合是一种has-a的关系。假设B组合了A,每个B对象中就都有A。
优先使用对象组合而不是类继承。
继承允许你根据基类的实现来定义派生类的实现
继承一定程度破坏了基类的封装
组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
哪几个成员函数不能被继承
构造函数,拷贝构造函数 析构函数 赋值运算符重载函数
什么函数不能被声明为虚函数
普通函数 构造函数 内联函数 静态成员函数 友元函数 不会被继承的基类的析构函数(没必要)
构造函数不能是虚函数的原因:当类中有虚函数时,编译器会生成一个虚函数表,虚函数表中存储虚函数地址。虚函数表是由编译器自动生成维护的,当存在虚函数时,每个类对象都会自动生成一个虚函数指针(vptr)指向虚函数表,在实现多态时,基类和派生类都有vptr; vptr的初始化:当对象创建时,编译器对vptr指针进行初始化,在定义派生类时,vptr先会指向基类的虚函数表,在基类构造完成之后,派生类就会指向自己的虚函数表。如果构造函数是虚函数的话,那么调用构造函数时就需要去找vptr,但是此时的vptr还没有初始化。
16.访问基类的私有虚函数
????
17.简述类成员函数的重写、重载和隐藏的区别
重载:
class A
{
public:
void fun(int tmp);
void fun(float tmp); // 重载 参数类型不同(相对于上一个函数)
void fun(int tmp, float tmp1); // 重载 参数个数不同(相对于上一个函数)
void fun(float tmp, int tmp1); // 重载 参数顺序不同(相对于上一个函数)
int fun(int tmp); // error: 'int A::fun(int)' cannot be overloaded 错误:**注意重载不关心函数返回类型**
}
隐藏:
#include <iostream>
using namespace std;
class Base
{
public:
void fun(int tmp, float tmp1) { cout << "Base::fun(int tmp, float tmp1)" << endl; }
};
class Derive : public Base
{
public:
void fun(int tmp) { cout << "Derive::fun(int tmp)" << endl; } // 隐藏基类中的同名函数
};
int main()
{
Derive ex;
ex.fun(1); // Derive::fun(int tmp)
ex.fun(1, 0.01); // error: candidate expects 1 argument, 2 provided
// ex.Base::fun1(1, 0.01); 正确写法
return 0;
}
重写:
#include <iostream>
using namespace std;
class Base
{
public:
virtual void fun(int tmp) { cout << "Base::fun(int tmp) : " << tmp << endl; }
};
class Derived : public Base
{
public:
virtual void fun(int tmp) { cout << "Derived::fun(int tmp) : " << tmp << endl; } // 重写基类中的 fun 函数
};
int main()
{
Base *p = new Derived();
p->fun(3); // Derived::fun(int) : 3
return 0;
}
重写和重载的区别:
范围区别:对于类中的函数的重载或者重写而言,重载在同一个类的内部,而重写则是在不同的类中(基类和派生类)
参数区别:重载的函数需要与原函数有相同的函数名,不同的参数列表,不关注函数的返回值类型;重写的函数的函数名,参数列表和返回值类型都必须和原函数相同,父类中被重写的函数需要 virtual 修饰。
virtual 关键字:重写的函数基类中必须有 virtual 关键字修饰,重载函数则不需要。
隐藏和重写、重载的区别:
范围却别:隐藏和与重载范围不同,隐藏在不同的类中(基类和派生类)
参数区别:隐藏函数和被隐藏的参数列表可以相同,也可以不相同,但函数名必须相同;当参数不同时,无论基类中函数是否被 virtual 修饰,基类函数都是被隐藏,而不是被重写。
18.多态的实现原理
1.C++ 分静态多态 和 动态多态
静态多态就是重载,在编译器决定,在编译时就确定函数地址。
动态多态就是通过继承重写基类的虚函数来实现的,因为是在运行时决定的,所以被称为动态多态。运行时在虚函数表中寻找到函数地址。
2.在基类的函数前加上 virtual 关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。
原理:
当一个类出现虚函数或者子类继承了虚函数后,就会在该类中产生一个虚函数表,虚函数表里的每一个元素是指向每一个虚函数的指针。被该类声明的对象还会包含一个虚函数表指针,指向该类的虚函数表地址。当一个类要调用虚函数时,会先将对象内存中的(vtable_ptr)虚函数表指针指向该类的虚函数表(vtable),虚函数表中在寻找到
里面的指针指向想用调用的虚函数,从而完成虚函数调用。从而实现动态联编。
19.数组和链表的区别, C++ array list vector 谈谈看法
存储形式不同:数组是一块连续的空间,声明时就要确定数组长度, 链表是一块可以不连续的空间,每个节点会保存相邻节点的指针。
数据查找:数组线性查找速度快,查找操作直接用偏移量就行。链表需要按顺序检索节点,效率较低。
数据插入或删除:链表可以快速删除和插入节点,而数据需要移动大量数据进行插入和删除。
越界问题:数组有越界行为,链表没有
数组便于查询,链表便于插入删除,数组长度固定,链表长度不固定
C++
array 大小固定,元素在内存中时连续存储的,使用时无法改变,只允许访问或者替换存储元素
list 双链表容器,前后靠指针维系,元素可以分散在内存中,不一定是连续存储
vector 底层是动态数组实现,元素在内存中连续,当需要扩容时,并不是在原有基础上简单的叠加,而是重新申请一块更大的内存,然后逐个复制过去,销毁旧的内存。(这个时候指向旧内存空间的迭代器失效,当操作容器时,需要及时更新)
顺序存储和链式存储
顺序存储:顺序存储是指在内存中开辟连续的存储空间来存放数据,代表:数组
优点:查询速度快,便利效率高
缺点:增加,删除效率低
链式存储:链式存储的存储空间不连续,他是通过指针来指向下一个元素或者上一个元素的地址来定位到该元素的,链式存储由数据域和指针域两部分组成,通过指针的穿插,从而形成了一个链表
优点:增加、删除效率高
缺点:遍历、查询效率低
20.链表反转
单链表反转:
void ReverseLinklist(struct Linklist *head)
{
if (NULL = head || NULL == head->next || NULL == head->next->next)
return ;
struct Linklist *begin = head->next;
struct Linklist *end = head->next->next;
while(end != NULL)
{
begin->next = end->next;
end->next = head->next;
head->next = end;
end = begin->next;
}
}
21.指针 和引用的区别
1.指针是一个变量,存储的是变量的地址,而引用是变量的别名
2.指针可以为空,引用不能为空,必须有实体
3.指针在初始化之后可以改变指向,而引用在初始化之后就不能改变
4.指针可以有多级,引用只有一级
5.sizeof 指针 得到的是指针的大小,而 sizeof 引用 得到的是引用所指向对象的大小
6.指针作为函数参数时,形参实际上是实参的一份拷贝,只是分别属于两个不同的指针变量,函数形参为引用时,能直接改变实参,他不拷贝实参,就是实参的一个别名
7.引用是类型安全,指针类型不安全(引用比指针多了类型检查)
22.&& 和 & 、|| 和 | 有什么区别
& 和 | 是对操作数进行求值运算,&& 和 || 是判断逻辑关系的
&& 在判断左操作数失败时就不会判断右操作数
|| 在判断左操作数成功时就不会判断右操作数
23.密码学知识
对称加密:加解密秘钥一致
AES,DES,3DES(3次DES加密,且三次秘钥不一样)
优点:速度快,效率高,计算量小
缺点:秘钥管理和分发困难,怎么在不安全的环境下分发秘钥是个问题,每一对用户的秘钥应该是不一样的,会使得收发双方秘钥数量巨大,管理有负担
非对称加密:加解密秘钥不一致,一般是公钥加密,私钥解密
RSA ECC
优点:安全性更高,公钥公开,私钥不公开
缺点:效率低,加解密时间长,只适合少量数据
加密模式
ECB: 电子密码本模式
CBC: 密码分组链接模式
PCBC: 填充密码块链接模式
CFB: 密文反馈模式
OFB: 输出反馈模式
CTR: 计数器模式
24.const 关键字作用
C语言
int a = 10;
const int * num = &a; //常量指针 不能通过这个指针改变常量 a 但是能通过引用来改变变量的值 a = 11; 这个指针能指向改变指向,指向别的地址
举例:
int a=5;
int b=6;
const int* n=&a;
n=&b; //正确
int a=5;
const int* n=&a;
a=6; //正确
const char * str = "12345";
str = "qwert"; //正确,还是指向常量,常量指针
str[2] = 'x'; //错误,不能改变常量
int * const num = &a; //指针常量 指针常量指向的地址不能改变,但是地址保存的数据可以改变 这个指针不能改变指向 *num = 11;这是允许的
举例:
int a=5;
int *p=&a;
int* const n=&a;
*p=8; //正确
char *const name = "abc";
name[2] = 'Z'; //这种是正确的,改变值
name = "bcde"; // 这种是错误的,name不能指向其他常量
修饰函数形参,修饰函数返回值(返回值只能赋值给被const 修饰的指针) 修饰全局变量(全局变量作用域是整个文件,一旦某个函数修改了管局变量的值,会影响到其他地方的使用,正常)
const #define 区别
#define 是预处理,在编译的时候就进行简单替换,不能进行类型检查 占用代码区空间,
const 关键字 是在编译运行时起作用 被修饰的就变成了常量
C++
const 引用:常量引用 不能修改引用对象
const 修饰成员函数被称为 常函数 (不能修饰全局函数,因为static 函数没有this指针) 举例: void test(void) const;
这是C++的规则,const修饰符用于表示函数不能修改成员变量的值,该函数必须是含有this指针的类成员函数,函数调用方式为thiscall,而类中的static函数本质上是全局函数,调用规约是__cdecl或__stdcall,不能用const来修饰它。一个静态成员函数访问的值是其参数、静态数据成员和全局变量,而这些数据都不是对象状态的一部分。而对成员函数中使用关键字const是表明:函数不会修改该函数访问的目标对象的数据成员。既然一个静态成员函数根本不访问非静态数据成员,那么就没必要使用const了。
该函数不能修改成员变量
不能调用非 const 成员函数,因为任何非 const 成员函数会有修改成员变量的企图
const 修饰对象
对象任何成员都不能被修改
const 类对象只能调用 const 成员函数
25.如何避免野指针
“野指针”产生的原因以及解决方法如下
1.指针类型变量声明时没有初始化。解决方法:声明时进行初始化,一般是指向NULL,或者直接指向具体地址值。
2.指针被free 或 delete 后,没被置为NULL。解决方法:指针被释放之后要置为NULL。
3.指针操作超越了变量的作用域。解决方法:在变量的作用域结束前释放掉变量,并指向NULL。
26.容器
vector 底层数据结构是什么 顺序表(动态数组),底层数据存储在一片连续空间上 查询复杂度 O(n * n)
vector优缺:
优:由于其顺序结构,它支持随机访问,读取更改效率很高。
缺:顺序结构随机插入删除时,需要挪动他元素,元素越多效率越低。( 还会使迭代器失效)
vector扩容:当数据量达到容器边缘时(end() == 边界),容器会自行扩容。(开辟一段连续空间,大小一般为原大小的2 / 1.5倍,将数据copy到新空间中,释放原空间,使用新空间)
std::map 底层是红黑树
时间复杂度
插入: O(logN)
查看:O(logN)
删除:O(logN)
std::unordered_map 底层是hash表
优先队列具有队列的所有特性,包括基本操作,只是在这基础上添加了内部的一个排序,它本质是一个堆实现的
举例:
#include<iostream>
#include <queue>
using namespace std;
int main()
{
//对于基础类型 默认是大顶堆
priority_queue<int> a;
//等同于 priority_queue<int, vector<int>, less<int> > a;
priority_queue<int, vector<int>, greater<int> > c; //这样就是小顶堆
priority_queue<string> b;
for (int i = 0; i < 5; i++)
{
a.push(i);
c.push(i);
}
while (!a.empty())
{
cout << a.top() << ' ';
a.pop();
}
cout << endl;
while (!c.empty())
{
cout << c.top() << ' ';
c.pop();
}
cout << endl;
b.push("abc");
b.push("abcd");
b.push("cbd");
while (!b.empty())
{
cout << b.top() << ' ';
b.pop();
}
cout << endl;
return 0;
}
输出
//4 3 2 1 0
//0 1 2 3 4
//cbd abcd abc pari的比较,先比较第一个元素,第一个相等比较第二个
无锁队列的原理(比较并交换)
27.TCP和UDP区别
TCP:传输控制协议(TCP,Transmission Control Protocol)面向连接的、可靠的、基于字节流的传输层协议
UDP:Internet 协议提供支持一个无连接的传输协议,称为(UDP,User Datagram Protocol)
区别:
1.TCP 面向连接,通过三次挥手建立连接,通过四次挥手解除连接; UDP是无连接的,即发送数据之前不需要建立连接,这种方式为 UDP 带来了高效的传输速率,但是也导致无法保证数据的发送成功。
2.TCP 面向字节流,实际上是 TCP 把数据看成一串无结构的字节流,由于连接问题,当网络出现波动时,连接可能出现响应问题。 UDP 是面向报文的,UDP 没有拥塞控制,因此网络出现拥塞不会影响原主机的发送速率。
3.每一个 TCP 连接都是点对点的。 UDP 不建立连接,可以支持一对一,多对一,多对多交互通信。
4.TCP 是可靠的通信方式。 TCP 连接传输的数据,TCP 通过超时重传,数据校验等手段来保证数据无差错,不丢失,不重复。 UDP 由于不需要建立连接, 将会以最大速度进行传输,但不保证交付可靠,可能会出现数据丢失,重复等问题。
5.TCP 的逻辑通信通道是全双工的可靠信道,而 UDP 是不可靠信道。
6.TCP 需要建立连接, UDP 不需要建立连接。
TCP 三次握手
1.clint 发送一个 SYN 包,告诉 server 端我的初始序列号是 X(seq=X);Clinet 进入 SYN-SNET(同步已发送状态)状态
2.server 收到 Clinet 的SYN 包,会回复一个 ACK包(确认包ACK=X+1),告诉 Client 已经收到了;Server 进入SYN-RCVD(同步收到状态)状态;接着 server 也告诉client自己的初始序列号,发送一个SYN包告诉Clinet自己的序列号是Y(seq=Y)
3.client 收到后,回复一个ACK包(确认包ACK=Y+1);确认收到,之后clinet 和 server 就进入ESTABLISHED(已建立连接状态)状态
TCP 四次挥手
1.client 发送断开 TCP 连接的请求报文(FIN = 1 表示要断开 TCP 连接, seq = x 初始序列号), clinet 进入FIN-WAIT-1(终止等待1)状态
2.server 回复 client 发送的 TCP 断开报文 (ACK = 1 seq = y初始序列号 ack = x + 1),server 进入CLOSE-WAIT(关闭等待)状态,此时server处于半关闭,client 及时没有数据要发送了, 但是server要是发送数据,client仍要接收
3.client 收到server发送的确认请求后,client 进入 FIN-WAIT-2(终止等待2)状态,等待server发送断开报文
4.server 向 client 发送断开报文(FIN = 1表示要断开, ack = x + 1 seq = z 新的序列号) server 进入 LAST—ACK(最后确认状态)状态
5.client 收到 server 断开报文,必须发出确认报文(ACK = 1, ack = z + 1, seq = x + 1) client 进入 TIME-WAIT(时间等待状态)状态,进过 2* MSL(最长报文寿命)的时间后,client 进入 close 状态
6.server 收到 client 的确认报文,立即进入 close 状态
四次挥手 server 会比 client 结束 TCP 连接时间早一些
28.OSI 7层模型 和 TCP/IP 模型
OSI 从上到下
应用层 http协议
表示层
会话层
传输层 TCP协议
网络层 IP协议
数据链路层
物理层
TCP/IP
应用层
传输层
网络层
网络接口层(数据链路层)
29.打开一个网页 都经过什么协议
1.DNS协议(应用层) 域名解析,获取相对应的IP
2.TCP协议 负责传输,与服务建立连接 传输层
3.IP协议 发送的数据在网络层使用IP协议
4.OPSF协议 IP数据包在路由器之间,路由器使用OPSF协议
5.ARP协议 路由器在与服务器通信时,将IP地址转换成MAC地址,需要使用ARP协议
6.http协议 建立连接成功后,使用HTTP协议访问网络,获取数据,收到数据后,通过HTTP协议来解析。
30. 进程和线程
进程:操作系统资源分配的最小单位
线程:操作系统能够进行运算调度的最小单位(包含在进程中)
区别:
1.根本区别:进程是操作系统进行资源分配的最小单元,线程是操作系统进行运算调度的最小单元
2.进程包含线程,线程属于进程,线程不包含进程
3.开销:进程的创建、销毁、切换都远大于线程
4.每个进程都拥有自己的内存和资源,而线程是要共享资源的
进程间通信方式
1.管道
2.信号量
3.套接字
4.消息队列
5.共享内存
线程间通信方式
1.锁机制,互斥锁、读写锁、条件锁(条件变量)、自旋锁
单线程和多线程:
单线程 按照顺序执行,如果发生阻塞,会影响到后续操作。
多线程
多线程优点:提升效率 ,避免阻塞,充分使用CPU(避免CPU空转)
多线程缺点:复杂的同步机制和加锁控制机制,要考虑竞争和同步问题,线程崩溃会影响程序(一个进程崩溃不影响其他进程,子进程崩溃也不会影响到主进程,因为每个进程有独立的系统资源。多线程比较脆弱,一个线程崩溃很可能影响到整个程序,因为多个线程是在一个进程里一起合作干活的。)
31.C语言源代码编译过程
1.预处理 生成 .i 文件
1.处理 #define 宏定义 将其替换掉
2.处理条件编译, #if、#ifdef、#elif、#else、#endif 等
3.处理#include 将被包含文件的内容插入到该命令所在的位置,这与复制粘贴的效果一样
4.删除注释
5.添加行号,文件名标识,方便调试
6.保留所有的#pragma命令,因为编译器需要使用它们
2.编译 生成 .s 文件
把预处理完的 .i 文件 转化成汇编代码文件
3.汇编 生成 .o 文件
将汇编代码文件转化成机器指令
4.链接
目标文件已经是二进制文件,与可执行文件的组织形式类似,只是有些函数和全局变量的地址还未找到,程序不能执行。链接的作用就是找到这些目标地址,将所有的目标文件组织成一个可以执行的二进制文件。
32.静态库 动态库
linux 下
静态库 libXXXXX.a 动态库 libXXXXXX.so
windows 下
静态库 .lib 动态库 .dll
静态库:在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。因此对应的链接方式称为静态链接。
在编译后的执行程序不再需要外部的函数库支持,运行速度相对快些
如果所使用的静态库发生更新改变,你的程序必须重新编译
静态库的代码是在编译过程中被载入程序中。
静态库:动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入
动态库的代码是当程序运行到相关函数才调用动态库的相应函数
动态库的改变并不影响你的程序,所以动态函数库升级比较方便;
因为函数库并没有整合进程序,所以程序的运行环境必须提供相应的库。
33.大小端字节序
数值例如0x2266使用两个字节存储;高位字节是0x22,低位字节是0x66
大端字节序:高位字节在前,低位字节在后,人类读写数值的方法
小端字节序:低位字节在前,高位字节在后,即以0x6622形式存储
网络字节序:大端
主机字节序:小端
0x1234567大端字节序和小端字节序的写法如下图:
34.字节对齐
struct A{
int i;
double b;
char c;
}
struct B{
int i;
char b;
double c;
}
struct C{
int i;
double b;
char c[7];
}
struct D{
int i;
double b;
char c[9];
}
union E{
int i;
double b;
char c[7];
}
union F{
int i;
double b;
char c[9];
}
enum G{
AAA,
BBB,
CCC
}
cout << sizeof(A) << endl; //24
cout << sizeof(B) << endl; //16
cout << sizeof(C) << endl; //24
cout << sizeof(D) << endl; //32
cout << sizeof(E) << endl; //8
cout << sizeof(F) << endl; //16
cout << sizeof(G) << endl; //4
35.cp -f
-f 选项:覆盖同名文件或目录时不进行提醒,直接强制覆盖。
但是有时候加了 -f 仍会问你是否覆盖,原因是什么?
因为有些Linux 发行版的cp命令 是 "cp -i" 的别名, -i: -f 选项相反,在覆盖目标文件之前给出提示,要求用户确认是否覆盖,回答 y 时目标文件将被覆盖
所以只有 用 alias 命令 把 cp 改回 "cp" 就行了 alias cp="cp"
36.which 、whereis、locate和find四者区别
which: 查找系统 PATH 目录下的可执行文件。其实就是查找那些已经安装好的可以直接执行的命令。
whereis: 定位某个命令的二进制文件、源码和帮助页文件;通过文件索引数据库来查找的而非PATH来查找的,所以查找的面比which要广
find: 通过搜索硬盘的方式查找
37.xargs 命令
echo "--help" | xargs cat // 相当于 cat --help
xargs -d 选项:
默认情况下xargs将其标准输入中的内容以空白(包括空格、Tab、回车换行等)分割成多个之后当作命令行参数传递给其后面的命令,并运行之,我们可以使用 -d 命令指定分隔符,例如:
echo '11@22@33' | xargs echo
输出:
11@22@33
默认情况下以空白分割,那么11@22@33这个字符串中没有空白,所以实际上等价于 echo 11@22@33 其中字符串 '11@22@33' 被当作echo命令的一个命令行参数
echo '11@22@33' | xargs -d '@' echo
输出:
11 22 33
指定以@符号分割参数,所以等价于 echo 11 22 33 相当于给echo传递了3个参数,分别是11、22、33
-p 选项
使用该选项之后xargs并不会马上执行其后面的命令,而是输出即将要执行的完整的命令(包括命令以及传递给命令的命令行参数),询问是否执行,输入 y 才继续执行,否则不执行。这种方式可以清楚的看到执行的命令是什么样子,也就是xargs传递给命令的参数是什么,例如:
echo '11@22@33' | xargs -p -d '@' echo
输出:
echo 11 22 33
?...y ==>这里询问是否执行命令 echo 11 22 33 输入y并回车,则显示执行结果,否则不执行
11 22 33 ==>执行结果
-n 选项
该选项表示将xargs生成的命令行参数,每次传递几个参数给其后面的命令执行,例如如果xargs从标准输入中读入内容,然后以分隔符分割之后生成的命令行参数有10个,使用 -n 3 之后表示一次传递给xargs后面的命令是3个参数,因为一共有10个参数,所以要执行4次,才能将参数用完。例如:
echo '11@22@33@44@55@66@77@88@99@00' | xargs -d '@' -n 3 echo
输出结果:
11 22 33
44 55 66
77 88 99
00
等价于:
echo 11 22 33
echo 44 55 66
echo 77 88 99
echo 00
实际上运行了4次,每次传递3个参数,最后还剩一个,就直接传递一个参数。
-E 选项,有的系统的xargs版本可能是-e eof-str
该选项指定一个字符串,当xargs解析出多个命令行参数的时候,如果搜索到-e指定的命令行参数,则只会将-e指定的命令行参数之前的参数(不包括-e指定的这个参数)传递给xargs后面的命令
echo '11 22 33' | xargs -E '33' echo
输出:
11 22
可以看到正常情况下有3个命令行参数 11、22、33 由于使用了-E '33' 表示在将命令行参数 33 之前的参数传递给执行的命令,33本身不传递。等价于 echo 11 22 这里-E实际上有搜索的作用,表示只取xargs读到的命令行参数前面的某些部分给命令执行。
注意:-E只有在xargs不指定-d的时候有效,如果指定了-d则不起作用,而不管-d指定的是什么字符,空格也不行。
echo '11 22 33' | xargs -d ' ' -E '33' echo => 输出 11 22 33
echo '11@22@33@44@55@66@77@88@99@00 aa 33 bb' | xargs -E '33' -d '@' -p echo => 输出 11 22 33 44 55 66 77 88 99 00 aa 33 bb
38.https 建立通信原理
1.client发送一个https的请求到服务端
2.server申请配置好的证书,包含公私钥
3.server将证书发送给client
4.client 解析完成,判断是否有异常,无异常的话,生成一个随机值(用于对称加密),用服务端公钥进行非对称加密随机值生成密文,client发送密文给server
5.server使用私钥非对称解密,得到随机值(建立成功)
6.双方用随机值当做共享秘钥进行对称加密明文 进行数据传输
39.多线程 锁
普通锁:互斥锁
读写锁:
写者:写者使用写锁,如果当前没有读者,也没有其他写者,写者立即获得写锁;否则写者将等待,直到没有读者和写者。
读者:读者使用读锁,如果当前没有写者,读者立即获得读锁;否则读者等待,直到没有写者。
特性:
1.同一时刻只有一个线程可以获得写锁,同一时刻可以有多个线程获得读锁。
2.读写锁出于写锁状态时,所有试图对读写锁加锁的线程,不管是读者试图加读锁,还是写者试图加写锁,都会被阻塞。
3.读写锁处于读锁状态时,有写者试图加写锁时,之后的其他线程的读锁请求会被阻塞,以避免写者长时间的不写锁。
自旋锁:
普通的锁,如果条件不达成就会进入睡眠,自旋锁,它不会放弃CPU时间片,而是不停的尝试获取锁,直到成功,适用于并发度不高,代码指向短的场景。
好处:阻塞和唤醒线程需要高昂的开销,在代码不复杂的情况下可能比转换线程带来的开销要大很多
坏处:最大坏处就是虽然避免了线程切换的开销,但是它会带来新的开销,因为他会不停的尝试去获得锁,如果这个锁一直没被释放,那这种尝试就是无效的,会浪费资源
自死锁:已经请求了自旋锁并得到,然后又请求锁
和信号量区别:
信号量:linux中的信号量是一种睡眠锁。如果有一个任务试图获得一个已经被持有的信号量时,信号量会将其推入等待队列,然后让其睡眠,这时处理器获得自由去执行其它代码。当持有信号量的进程将信号量释放后,在等待队列中的一个任务被唤醒,从而可以获得此信号量。
1.自旋锁不会引起调用者睡眠,如果自旋锁已被别的执行单元保持,调用者就一直循环查看该自旋锁的保持者是否已释放了锁。
2.信号量会引起调用者睡眠,它会把进程从运行队列上拖出去让其睡眠,除非获得锁。
需求 建议的加锁方法
低开销加锁 优先使用自旋锁
短期锁定 优先使用自旋锁
中断上下文加锁 使用自旋锁
长期加锁 优先使用信号量
持有锁时需要睡眠 使用信号量
40.设计模式
三大类:
创建型模式:对类的实例化过程进行抽象, 能够将软件模块中对象的创建和对象的使用分离
简单工厂模式,工程方法模式,抽象工厂模式,单例模式,建造者模式
结构性模式:关注于对象的组成以及对象之间的依赖关系,描述如何将类和对象如何结合在一起形成更大的结构,就像 搭积木 ,可以通过简单积木的组合形成复杂的、功能强大的结构。
适配器模式,装饰者模式,代理模式,外观模式,桥接模式,组合模式,享元模式
行为型模式:关注于对象的行为问题,是对在不同的对象之间划分责任和算法的抽象化;不仅仅关注类和对象的结构,而且重点关注他们之间的相互作用。
策略模式,模板方法模式,观察者模式,迭代器模式,责任链模式,命令模式,备忘录模式,状态模式,访问者模式,中介者模式,解释器模式。
六大原则
总原则 开闭原则:一个软件实体,如类、模块和函数应该对扩展开放,对修改关闭
单一职责原则:一个类应该只有一个发生变化的原因。
里氏替换原则:所有引用基类的地方必须能透明地使用其子类的对象
依赖倒置原则:上层模块不应该依赖底层模块,它们都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象
接口隔离原则:客户端不应该依赖它不需要的接口;类间的依赖关系应该建立在最小的接口上。
迪米特法则(最少知道原则):一个类对自己依赖的类知道的越少越好。无论被依赖的类多么复杂,都应该将逻辑封装在方法的内部,通过public方法提供给外部。这样当被依赖的类变化时,才能最小的影响该类。
合成复用原则:尽量使用对象组合/聚合,而不是继承关系达到软件复用的目的
41.动态库导入导出
在生成dll的时候我们希望将我们的符号导出(符号就是程序中定义的变量或者方法),在使用dll时则时希望导入符号。通常由两种方式来实现符号表的导入导出。
_declspec(dllimport) 导入 和 __declspec(dllexport) 导出
_declspec(dllexport)与_declspec(dllimport)是相互呼应,只有在DLL内部用dllexport作了声明,才能 在外部函数中用dllimport导入相关代码。实际上,在应用程序访问DLL时,实际上就是应用程序中的导入函数与DLL文件中的导出函数进行链接。而 且链接的方式有两种:隐式迎接和显式链接。
隐式链接是指通过编译器提供给应用程序关于DLL的名称和DLL函数的链接地址,面在应用程序中不需要显式地将DLL加载到内存,即在应用程序中使用dllimport即表明使用隐式链接。不过不是所有的隐式链接都使用dllimport。
显式链接刚同应用程序用语句显式地加载DLL,编译器不需要知道任何关DLL的信息
42.浅拷贝和深拷贝
浅拷贝:只复制某个对象的指针, 而不复制对象本身, 新旧对象还是共享同一块内存.(在类中有指针类型成员变量时,一定要重写拷贝构造函数和赋值操作符,不能使用默认的,原因是因为不重写的话,是浅拷贝,当一个对象释放后,另外一个对象在释放就报错了)
深拷贝:在拷贝的过程中会另外创造一个一模一样的对象. 新对象跟原对象不共享内存, 修改新对象不会改到原对象
43.设计模式---工厂方法模式
。在工厂方法模式中,工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象,这样做的目的是将产品类的实例化操作延迟到工厂子类中完成,即通过工厂子类来确定究竟应该实例化哪一个具体产品类。
优点:在工厂方法模式中,工厂方法用来创建客户所需要的产品,同时还向客户隐藏了哪种具体产品类将被实例化这一细节,用户只需要关心所需产品对应的工厂,无须关心创建细节,甚至无须知道具体产品类的类名。
基于工厂角色和产品角色的多态性设计是工厂方法模式的关键。它能够使工厂可以自主确定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部。工厂方法模式之所以又被称为多态工厂模式,是因为所有的具体工厂类都具有同一抽象父类。
使用工厂方法模式的另一个优点是在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改其他的具体工厂和具体产品,而只要添加一个具体工厂和具体产品就可以了。这样,系统的可扩展性也就变得非常好,完全符合“开闭原则”。
44.模板属于静态多态
45.线程池
我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务呢?
线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件), 则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。
线程池的组成结构
任务队列:存储需要处理的任务,由工作的线程来处理这些任务
1.通过线程池提供的 API 函数,将一个待处理的任务添加到任务队列,或者从任务队列中删除
2.已处理的任务会从任务队列中删除
3.线程池的使用者,也就是调用线程池函数往任务队列中添加任务的线程就是生产者线程
工作线程:任务队列任务的消费者,N 个
1.线程池中维护了一定数量的工作线程,他们的作用是不停的读任务队列,从里边取出任务并处理
2.工作的线程相当于是任务队列的消费者角色
3.如果任务队列为空,工作的线程将会被阻塞(使用条件变量/信号量阻塞)
4.如果阻塞之后有了新的任务,由生产者将阻塞解除,工作线程开始工作
管理者线程:不处理任务队列中的任务,1个
1.它的任务是周期性的对任务队列中的任务数量以及处于忙状态的工程线程个数进行检测
2.当任务过多的时候,可以适当的创建一些新的工作线程
3.当任务过少的时候,可以适当的销毁一些工作线程
46.多线程和栈
47.设计模式 - 单例模式
48.SDK封装
为什么标准头文件都有类似以下的结构?
#ifndef __INCvxWorksh
#define __INCvxWorksh
#ifdef __cplusplus
extern "C" {
#endif
#ifdef __cplusplus
}
#endif
#endif
显然,头文件中的编译宏“#ifndef __INCvxWorksh、#define __INCvxWorksh、#endif” 的作用是防止该头文件被重复引用。
被extern "C"限定的函数或变量是extern类型的;
extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用
被extern "C"修饰的变量和函数是按照C语言方式编译和连接的
不修饰直接C++ 直接使用C会报错,
49.路由和交换的区别 概念
路由谋短,交换求快。
交换机工作于数据链路层,用来隔离冲突域,连接的所有设备同属于一个广播域(子网),负责子网内部通信。
路由器工作于网络层,用来隔离广播域(子网),连接的设备分属不同子网,工作范围是多个子网之间,负责网络与网络之间通信。
工作层次不同:
拿OSI七层模型来说,从底往上以此是:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。
而交换机主要工作在数据链路层(第二层)
路由器工作在网络层(第三层)。
转发依据不同:
交换机转发所依据的对象时:MAC地址。(物理地址)
路由转发所依据的对象是:IP地址。(网络地址)
主要功能不同:
交换机主要用于组建局域网,
而路由主要功能是将由交换机组好的局域网相互连接起来,或者接入Internet。
交换机能做的,路由都能做。
交换机不能分割广播域,路由可以。
路由还可以提供防火墙的功能。
路由配置比交换机复杂。
价格不同
交换机是看门大爷,路由是邮差。
50.单片机原理
(一)主要结构:CPU(中央处理器)、RAM(数据存储器/随机存取存储器)、ROM(程序存储器/只读存储 器)、定时/计数器、I/O口。
(二)单片机的特点及应用
(1) 特点:
1.集度高,体积小,抗干扰能力强,可靠性高。
2.开发性能好,开发周期短,控制能力强。
3.低功耗、低电压,具有掉电保护功能。
4.通用性和灵活性较好。
5.性价比高。
51.构造函数不能为虚函数
虚函数的调用需要虚函数表指针,该指针存放在对象的内存空间中,若构造函数生命为虚函数,那么由于对象还没创建,没有内存空间,更没有指向虚函数表的虚函数表指针,就无法调用虚构造函数,若构造函数为虚函数的话,首先就会调用虚函数表指针,但是现在对象还没创建,根本就没有改指针,无法操作。