构造函数#
- 函数名与类名相同
- 不能定义返回值,也不能有return语句
- 可以有形式参数,也可以没有
- 可以是内联函数
- 可以重载
- 可以带默认参数值
- 对象被创建时自动调用
- 如果不定义构造函数,编译器编译阶段会生成默认构造函数
- 如果此时希望编译器隐含生成默认构造函数,只需加上=default
1 | class Clock { |
委托构造函数#
1 | Clock::Clock(int h,int m,int s):hour(h),minute(m),second(s){} |
拷贝构造函数#
特殊的构造函数,其形参为本类对象的引用, 用一个已存在的对象去初始化同类型的新对象 1. 定义一个对象,以本类 另一个对象作为初始值,发送拷贝构造函数 2. 如果函数的形参时类的对象,调用函数时,将使用实参对象初始化形参对象 3. 如果函数的 返回值是类的对象,函数执行完返回主调函数时,将使用return语句中的对象初始化一个临时无名对象,传递给主调i函数 4. C++11用"=delete"指示编译器不生成默认复制构造函数 5. 拷贝构造函数是一种特殊的构造函数,具有 单个形参,该形参(常用const修饰)是 对该类类型的引用。 6. 当定义一个新对象并用一个同类型的对象对它进行初始化时,将 显示使用拷贝构造函数。当该类型的对象传递给函数或从函数返回该类型的对象时,将 隐式调用拷贝构造函数**。
C++支持两种初始化形式:复制初始化(int a = 5;)和直接初始化(int
a(5);)对于类
类型直接初始化直接调用实参匹配的构造函数,复制初始化总是调用拷贝构造函数,也就是说:
1
2A x(2); //直接初始化,调用构造函数
A y = x; //复制初始化,调用拷贝构造函数
下面使用上面定义的类对象来说明各个构造函数的用法:
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 void main()
2 {
3 // 调用了无参构造函数,数据成员初值被赋为0.0
4 Complex c1,c2;
5
6 // 调用一般构造函数,数据成员初值被赋为指定值
7 Complex c3(1.0,2.5);
8 // 也可以使用下面的形式
9 Complex c3 = Complex(1.0,2.5);
10
11 // 把c3的数据成员的值赋值给c1
12 // 由于c1已经事先被创建,故此处不会调用任何构造函数
13 // 只会调用 = 号运算符重载函数
14 c1 = c3;
15
16 // 调用类型转换构造函数
17 // 系统首先调用类型转换构造函数,将5.2创建为一个本类的临时对象,然后调用等号运算符重载,将该临时对象赋值给c1
18 c2 = 5.2;
19
20 // 调用拷贝构造函数( 有下面两种调用方式)
21 Complex c5(c2);
22 Complex c4 = c2; // 注意和 = 运算符重载区分,这里等号左边的对象不是事先已经创建,故需要调用拷贝构造函数,参数为c2
23
24 }
深拷贝和浅拷贝:#
所谓浅拷贝,指的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。
在“深拷贝”的情况下,对于对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间
如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝
上面提到,如果没有自定义拷贝构造函数,则系统会创建默认的拷贝构造函数,但系统创建的
默认拷贝构造函数只会执行“浅拷贝”,即将被拷贝对象的数据成员的值一一赋值给新创建的对象,若该类的数据成员中
有指针成员,则会使得新的对象的指针所指向的地址与被拷贝对象的指针所指向的地址相同,delete该指针时则会导致两次重复delete而出错。下面是示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61 1 #include <iostream.h>
2 #include <string.h>
3 class Person
4 {
5 public :
6
7 // 构造函数
8 Person(char * pN)
9 {
10 cout << "一般构造函数被调用 !\n";
11 m_pName = new char[strlen(pN) + 1];
12 //在堆中开辟一个内存块存放pN所指的字符串
13 if(m_pName != NULL)
14 {
15 //如果m_pName不是空指针,则把形参指针pN所指的字符串复制给它
16 strcpy(m_pName ,pN);
17 }
18 }
19
20 // 系统创建的默认复制构造函数,只做位模式拷贝
21 Person(Person & p)
22 {
23 //使两个字符串指针指向同一地址位置
24 m_pName = p.m_pName;
25 }
26
27 ~Person( )
28 {
29 delete m_pName;
30 }
31
32 private :
33 char * m_pName;
34 };
35
36 void main( )
37 {
38 Person man("lujun");
39 Person woman(man);
40
41 // 结果导致 man 和 woman 的指针都指向了同一个地址
42
43 // 函数结束析构时
44 // 同一个地址被delete两次
45 }
46
47
48 // 下面自己设计复制构造函数,实现“深拷贝”,即不让指针指向同一地址,而是重新申请一块内存给新的对象的指针数据成员
49 Person(Person & chs);
50 {
51 // 用运算符new为新对象的指针数据成员分配空间
52 m_pName=new char[strlen(p.m_pName)+ 1];
53
54 if(m_pName)
55 {
56 // 复制内容
57 strcpy(m_pName ,chs.m_pName);
58 }
59
60 // 则新创建的对象的m_pName与原对象chs的m_pName不再指向同一地址了
61 }
一个空的class类里有什么?#
定义一个空类
1
2
3class Empty
{
};1
2
3Empty()
{
}1
2
3Empty(const Empty& copy)
{
}1
2
3Empty& operator = (const Empty& copy)
{
}1
2
3~Empty()
{
}
先假设我们有个很傲娇的类,它不希望通过构造函数生成,也不希望别的对象对它赋值。
然而更加高级的做法是定义一个空类,将空类的复制构造函数和赋值操作符声明为私有,然后让SomeClass继承它。像这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14class Empty
{
protected:
Empty(){} //允许derived class调用
~Empty(){}
private:
Empty(const Empty&); //阻止了copying
Empty& operator = (const Empty&);
};
class SomeClass: private Empty
{
...
};
虚函数的作用以及实现原理#
链接:https://www.nowcoder.com/questionTerminal/1f67d4e2b6134c298e993e622181b333 #### 虚函数的作用:简单讲即实现多态。 基类定义了虚函数,子类可以重写该函数,当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态地调用属于子类的该函数,且这样的函数调用是无法在编译器期间确认的,而是在运行期确认,也叫做迟绑定。
对于虚函数的支持则分两步完成:
1.每一个class产生一堆指向虚函数的指针,放在表格之中。这个表格称之为虚函数表(virtual table,vtbl)。
2.每一个对象被添加了一个指针,指向相关的虚函数表vtbl。通常这个指针被称为vptr。vptr的设定(setting)和重置(resetting)都由每一个class的构造函数,析构函数和拷贝赋值运算符自动完成。
另外,虚函数表地址的前面设置了一个指向type_info的指针,RTTI(Run Time Type Identification)运行时类型识别是有编译器在编译器生成的特殊类型信息,包括对象继承关系,对象本身的描述,RTTI是为多态而生成的信息,所以只有具有虚函数的对象在会生成。
C++中虚函数使用虚函数表和 虚函数表指针实现,虚函数表是一个类的虚函数的地址表,用于索引类本身以及父类的虚函数的地 址,假如子类的虚函数重写了父类的虚函数,则对应在虚函数表中会把对应的虚函数替换为子类的 虚函数的地址;虚函数表指针存在于每个对象中(通常出于效率考虑,会放在对象的开始地址处), 它指向对象所在类的虚函数表的地址;在多继承环境下,会存在多个虚函数表指针,分别指向对应 不同基类的虚函数表。
简单来讲是多态,也就是允许派生类对象指向基类指针在运行时调用调用派生类的同名函数。 原理:含有虚函数的类对象,在啊创建时会再头部创建一个指针,指向一个虚表,虚表内保存着虚函数的地址,当调用虚函数时,调用指针指向虚表,子啊虚表中找到虚函数的地址。从而实现运行时多态,普通的成员函数地址是固定的,直接调用即可。
Overload、Overwrite及Override的区别#
Overload(重载):在C++程序中,可以将语义、功能相似的几个函数用同一个名字表示,但参数或返回值不同(包括类型、顺序不同),即函数重载。 1. 相同的范围(在同一个类中); 2. 函数名字相同; 3. 参数不同; 4. virtual 关键字可有可无。
Override(覆盖):是指派生类函数覆盖基类函数,特征是: 1. 不同的范围(分别位于派生类与基类); 2. 函数名字相同; 3. 参数相同; 4. 基类函数必须有virtual 关键字。
Overwrite(重写):是指派生类的函数屏蔽了与其同名的基类函数,规则如下: 1. 如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。 2. 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
1 | #include <stdio.h> |
overload的话,只有函数返回值类型不同,会重载吗?#
在使用重载时只能通过不同的参数样式。例如,不同的参数类型,不同的参数个数,不同的参数顺序(当然,同一方法内的几个参数类型必须不一样,例如可以是fun(int, float), 但是不能为fun(int, int));
不能通过访问权限、返回类型、抛出的异常进行重载;
方法的异常类型和数目不会对重载造成影响;
对于继承来说,如果某一方法在父类中是访问权限是priavte,那么就不能在子类对其进行重载,如果定义的话,也只是定义了一个新方法,而不会达到重载的效果。
一个结构体中有一个int,一个char,一个static int,问这个结构体占多少内存#
结构体 内存对齐规则 结构体所占用的内存 与其成员在结构体中的声明顺序有关,其成员的内存对齐规则如下:
每个成员分别按自己的对齐字节数和PPB(指定的对齐字节数,32位机默认为4)两个字节数最小的那个对齐,这样可以最小化长度。
复杂类型(如结构)的默认对齐方式是它最长的成员的对齐方式,这样在成员是复杂类型时,可以最小化长度。
结构体对齐后的长度必须是成员中最大的对齐参数(PPB)的整数倍,这样在处理数组时可以保证每一项都边界对齐。
计算结构体的内存大小时,应该列出每个成员的偏移地址,则其长度=最后一个成员的偏移地址+最后一个成员数的长度+最后一个成员的调整参数
STL中有什么类#
https://blog.csdn.net/chuanzhouxiao/article/details/51902786
1
2
3
4
5
6
7
8
9
10
11
12vector 向量
string 字符串
list 列表
queue 队列
map 映射
set 集合
stack 栈
map,vector和unordered_map区别及实现原理
红黑树 hash表
mysql索引
tcp三次握手 重传机制
1.进程和线程的定义#
- 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位.
- 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行.
2.进程和线程的区别#
- 进程在执行过程中拥有独立的内存单元,而该进程的多个线程共享内存,从而极大地提高了程序的运行效率。
- 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
- 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。
- 在很多现代操作系统中,一个进程的(虚)地址空间大小为4G,分为系统(内核?)空间和用户空间两部分,系统空间为所有进程共享,而用户空间是独立的,一般WINDOWS进程的用户空间为2G。
- 一个进程中的所有线程共享该进程的地址空间,但它们有各自独立的(/私有的)栈(stack),Windows线程的缺省堆栈大小为1M。堆(heap)的分配与栈有所不同,一般是一个进程有一个C运行时堆,这个堆为本进程中所有线程共享,windows进程还有所谓进程默认堆,用户也可以创建自己的堆。 用操作系统术语,线程切换的时候实际上切换的是一个可以称之为线程控制块的结构(TCB?),里面保存所有将来用于恢复线程环境必须的信息,包括所有必须保存的寄存器集,线程的状态等。
3.多线程的适用场景是什么?为啥要用多线程?#
使用多线程是为了 提高程序运行的效率。假如有一个程序,要求用户输入多个算式,计算出结果,并分别打印到屏幕上。如果用户一直没有输入,那么无法计算,更无法打印。如果用户输入了,必须要全部输入完,才能计算出结果,再打印到屏幕。 使用线程的话,一个线程用来等待用户输入,一个用来计算结果,一个用来打印。用户在输入算式3的时候,计算线程在计算算式2,打印线程在打印算式1,三个线程同时进行,减少了等待,这样就提高了运行效率
4.堆和栈#
堆:是 共有的空间,分全局堆和局部堆。全局堆就是 所有没有分配的空间,局部堆就是 用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是记得 用完了要还给操作系统,要不然就是内存泄漏。
栈:是个 线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是 thread safe的。操作系统在切换线程的时候会自动的切换栈,就是切换 SS/ESP寄存器。栈空间不需要在高级语言里面显式的分配和释放。 函数调用栈堆
Top N排序 拷贝构造函数 复制构造函数 static 关键字
源文件cpp到可执行文件exe的过程#
从一个cpp文件到一个exe文件,大概经历了以下过程:
预处理(preprocessor) 根据预处理命令组装成新的C/C++程序,常以i为扩展名。这个过程包括:宏的替换、删除注释、处理预处理指令(如#include、#ifdef)。
编译(complier) 将得到的i文件翻译成汇编代码,即.s文件。
汇编(assembler) 将汇编文件翻译成机器指令,并打包成可重定位目标程序的o文件。该文件是二进制文件,字节编码是机器指令。编译器把一个cpp编译汇编得到目标文件时,除了要在目标文件里写入cpp里包含的数据和代码,还要至少提供3个表:
- 未解决符号表(unresolved symbol table):提供所有在编译单元里引用但定义不在本编译单元里的符号及其出现的地址;
- 导出符号表(export symbol table):提供本编译单元具有定义,且愿意提供给其它编译单元使用的符号及其地址(全局作用域);
- 地址重定向表(address redirect table):提供本编译单元所有对自身地址的引用的记录。
- 链接(linker) 由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或函数调用)或程序中可能调用了某个库文件中的函数。将引用的其它o文件并入到我们程序所在的o文件中并进行处理,方可得到最终的可执行文件。 链接器进行链接的时候,首先决定各个目标文件在最终可执行文件里的位置。然后访问所有目标文件的地址重定向表,对其中记录的地址进行重定向(即加上该编译单元实际在可执行文件里的起始地址)。然后遍历所有目标文件的未解决符号表,并且在所有的导出符号表里查找匹配的符号,并在未解决符号表中所记录的位置上填写实际的地址(也要加上拥有该符号定义的编译单元实际在可执行文件里的起始地址)。最后把所有的目标文件的内容写在各自的位置上,再做一些别的工作,即得到一个可执行文件。 PS:实际链接的时候更为复杂,因为实际的目标文件里把数据或代码分为好几个区,重定向等要按区进行,但原理一样。
- 内部链接:一个名称对编译单元(cpp文件)来说是局部的,在链接的时候其它的编译单元无法链接到它;
- 外部链接:一个名称对编译单元来说不是局部的,在链接的时候其它的编译单元可以访问它,即它可以和别的编译单元交互。
const和define#
const定义的只读变量在程序运行过程中只有一份拷贝(因为它是全局的只读变量,存放在静态区),而#define定义的宏常量在内存中有若干个拷贝。
#define宏是在预编译阶段进行替换,而const修饰的只读变量是在编译的时候确定其值。
#define宏没有类型,而const修饰的只读变量具有特定的类型
1
2
3
4const int *p; //p可变,p指向的对象不可变
int const*p; //p可变,p指向的对象不可变
int *const p; //p不可变,p指向的对象可变
const int *const p; //指针p和p指向的对象都不可变
这里有一个记忆和理解的方法:
先忽略类型名(编译器解析的时候也是忽略类型名),我们看const离哪个近。"近水楼台先得月",离谁近就修饰谁。
判断时忽略括号中的类型
1
2
3
4const (int) *p; //const修饰*p,*p是指针指向的对象,不可变
(int) const *p; //const修饰*p,*p是指针指向的对象,不可变
(int)*const p; //const修饰p,p不可变,p指向的对象可变
const (int) *const p; //前一个const修饰*p,后一个const修饰p,指针p和p指向的对象都不可变
new和malloc的区别#
https://www.cnblogs.com/engraver-lxw/p/8600816.html 1. 申请的内存所在位置 new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。 2. 返回类型安全性 new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是**返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型**。
- 内存分配失败时的返回值 new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。
在使用C语言时,我们习惯
在malloc分配内存后判断分配是否成功:
1
2
3
4
5
6
7
8
9int *a = (int *)malloc ( sizeof (int ));
if(NULL == a)
{
...
}
else
{
...
} 1
2
3
4
5
6
7
8try
{
int *a = new int();
}
catch (bad_alloc)
{
...
}
- 是否需要指定内存大小 使用
new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,而
malloc则需要显式地指出所需内存的尺寸。
1
2
3class A{...}
A * ptr = new A;
A * ptr = (A *)malloc(sizeof(A)); //需要显式指定所需内存大小sizeof(A); - 是否调用构造函数/析构函数 使用new操作符来分配对象内存时会经历三个步骤:
- 第一步:调用operator new 函数(对于数组是operator new[])分配一块足够大的,原始的,未命名的内存空间以便存储特定类型的对象。
- 第二步:编译器运行 相应的构造函数以构造对象,并为其传入初值。
- 第三步:对象构造完成后,返回一个指向该对象的指针。
使用delete操作符来释放对象内存时会经历两个步骤: - 第一步:调用 对象的析构函数。 - 第二步:编译器 调用operator delete(或operator delete[])函数释放内存空间。
总之来说,new/delete会调用对象的构造函数/析构函数以完成对象的构造/析构。而 malloc则不会。
- 对数组的处理 C++提供了new[]与delete[]来专门处理数组类型:
使用new[]分配的内存必须使用delete[]进行释放:
1
A * ptr = new A[10];//分配10个A对象
new对数组的支持体现在它会分别调用构造函数函数初始化每一个数组元素,释放对象时为每个对象调用析构函数。注意delete[]要与new[]配套使用,不然会找出数组对象部分释放的现象,造成内存泄漏。至于malloc,它并知道你在这块内存上要放的数组还是啥别的东西,反正它就给你一块原始的内存,在给你个内存的地址就完事。所以如果要动态分配一个数组的内存,还需要我们手动自定数组的大小:1
delete [] ptr;
1
int * ptr = (int *) malloc( sizeof(int)* 10 );//分配一个10个int元素的数组
- new与malloc是否可以相互调用 operator new /operator
delete的实现可以基于malloc,而
malloc的实现不可以去调用new。下面是编写operator new
/operator delete 的一种简单方式,其他版本也与之类似:
1
2
3
4
5
6
7
8
9
10
11void * operator new (sieze_t size)
{
if(void * mem = malloc(size)
return mem;
else
throw bad_alloc();
}
void operator delete(void *mem) noexcept
{
free(mem);
}
指针对齐#
以下代码打印的结果是(假设运行在 64 位计算机上):
1
2
3
4
5
6
7
8struct st_t {
int status;
short *pdata;
char errstr[32];
};
st_t st[16];
char *p=(char *)(st[2].esstr+32);
printf(“%d”,(p-(char *)(st)));
根据字节对齐,在64位系统下struct st_t 结构体占用的字节为48个。
1
2
3
4
5
6
7struct st_t {
int status; //占用8个(后面的4个为对齐位)
short *pdata;//占用8个
char errstr[32];//占用32个
};
char *p=(char *)(st[2].esstr+32),p实际指向了st[3]
则p-(char *)(st)),即为&st[3]-&st[0],占用空间为3个结构体的大小,即3*48=144
空类的sizeof大小,有一个虚函数的类的sizeof#
https://blog.csdn.net/foreverhuylee/article/details/39320977
题目(二):运行下面的代码,输出是什么?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23class A
{
};
class B
{
public:
B() {}
~B() {}
};
class C
{
public:
C() {}
virtual ~C() {}
};
int _tmain(int argc, _TCHAR* argv[])
{
printf("%d, %d, %d\n", sizeof(A), sizeof(B), sizeof(C));
return 0;
}
答案是1, 1, 4。class A是一个空类型,它的实例不包含任何信息,本来求sizeof应该是0。但当我们声明该类型的实例的时候,它必须在内存中占有一定的空间,否则无法使用这些实例。至于占用多少内存,由编译器决定。Visual Studio 2008中每个空类型的实例占用一个byte的空间。
class B在class A的基础上添加了构造函数和析构函数。由于构造函数和析构函数的调用与类型的实例无关(调用它们只需要知道函数地址即可),在它的实例中不需要增加任何信息。所以sizeof(B)和sizeof(A)一样,在Visual Studio 2008中都是1。
class C在class B的基础上把析构函数标注为虚拟函数。C++的编译器一旦发现一个类型中有虚拟函数,就会为该类型生成虚函数表,并在该类型的每一个实例中添加一个指向虚函数表的指针。在32位的机器上,一个指针占4个字节的空间,因此sizeof(C)是4。
值传递,引用传递#
值传递---单向传递 swap(int a,int b) 引用传递 ----双向传递 swap(int
&a,int &b)
引用即别名,必须初始化
内联函数 inline#
作用:可避免函数调用的开销 注:内联函数只是对编译器发送一个请求,编译器可以忽略该请求 一般用于优化规模小,流程直接,频繁调用的函数
- 内联函数体内不能有循环语句和switch语句
- 内联函数定义必须在内联函数第一次调用之前
- 对内联函数不能进行异常接口声明
constexpr函数#
指能用于常量表达式的函数
1
2
3
4
5
6constexpr int getsize() {
return 20;
}
int main() {
constexpr int foo = getsize(); //foo是常量表达式
}1
2
3const int max_files=20; //常量表达式
const int limit=max_files+1; //常量表达式
int staff_size=27; //不是常量表达式
默认参数值#
有默认参数的形参必须列在函数形参列表的最右端
调用实参与形参结合的次序是从左到右
1
2
3
4
5int add(int x,int y=5,int z=6);//正确
add(1)=12;
add(1,2)=9;
int add(int x=1,int y=5,int z);//错误