1.conversion funciton 转换函数
eg:分数转为double
2.non-explicit-one-argument ctor
别的东西转换为分数
Fraction(int num, int den=1)
3.pointer like class
4.function like class
5.namespace 经验谈
6.class template 类模板
7.函数模板
8.成员模板
reference
ps: 引用其实是一个指针,是四个字节,但是为了符合我们的逻辑:sizeof®sizeof(x) &x&r 我们会让引用的大小和x类型的大小一致。(全都是假象)
9.虚函数
class B override 了class 的虚函数vfunc1( )
动态绑定的三个条件:
1.通过指针
2.通过虚函数
3.向上转型
关于this
ps: 成员函数都默认带着this参数
符合动态绑定的三个条件
关于动态绑定
ps: a.vfunc1()符合动态绑定的其它两个条件,但是不符合通过指针调用这个条件,因为a是一个对象。所以其是静态绑定而非动态绑定
谈谈const
构造函数和析构函数能否为虚函数
答: 构造函数不能为虚函数,而析构函数可以且常常是虚函数
构造函数不能为虚函数:
这就要涉及到C对象的构造问题了,C对象在三个地方构建:(1)函数堆栈;(2)自由存储区,或称之为堆;(3)静态存储区。无论在那里构建,其过程都是两步:首先,分配一块内存;其次,调用构造函数。好,问题来了,如果构造函数是虚函数,那么就需要通过vtable 来调用,但此时面对一块 raw memeory,到哪里去找 vtable 呢?毕竟,vtable 是在构造函数中才初始化的啊,而不是在其之前。因此构造函数不能为虚函数。
ps:
vtable(虚函数表)是类所有的,类的所有对象共享,在编译时期就确定了下来,存储在在只读数据段(常量区);而vptr虚表指针才是每个对象自己所有的,在构造函数中进行空间分配的。因此我觉得原因之一有:虚函数的调用需要用到对象的虚表指针,而虚表指针在为对象分配实例(构造函数阶段)中进行空间分配;因此将构造函数定义为虚函数时,此时虚表指针尚未创建,也就无法去调用“虚函数定义下的构造函数”。
析构函数为虚函数:
析构函数可以为虚函数,主要是基类指针指向子类对象的情况下,在基类销毁时,只调用基类的析构函数而不调用子类的析构函数,从而导致内存泄漏,所以需要虚函数机制来帮助系统’ 识别 '需要释放资源。
虚函数指针和虚函数表的创建时机:
对于虚函数表来说,在编译的过程中编译器就为含有虚函数的类创建了虚函数表,并且编译器会在构造函数中插入一段代码,这段代码用来给虚函数指针赋值。因此虚函数表是在编译的过程中创建。
对于虚函数指针来说,由于虚函数指针是基于对象的,所以对象在实例化的时候,虚函数指针就会创建,所以是在运行时创建。由于在实例化对象的时候会调用到构造函数,所以就会执行虚函数指针的赋值代码,从而将虚函数表的地址赋值给虚函数指针。
静态成员函数为什么不能是虚函数
首先什么是static静态成员函数?静态成员函数不属于类中的任何一个对象和实例,属于类共有的一个函数。也就是说,它不能用this指针来访问,因为this指针指向的是每一个对象和实例。
对于virtual虚函数,它的调用恰恰使用this指针。在有虚函数的类实例中,this指针调用vptr指针,指向的是vtable(虚函数列表),通过虚函数列表找到需要调用的虚函数的地址。总体来说虚函数的调用关系是:this指针->vptr(4字节)->vtable ->virtual虚函数。
所以说,static静态函数没有this指针,也就无法找到虚函数了。所以静态成员函数不能是虚函数。他们的关键区别就是this指针
STL底层数据结构及特点
顺序容器:顺序访问元素的容器
Vector: 底层数据结构为数组,随机访问O(1),插入和删除O(n),扩容时会申请2倍当前的空间
List:底层数据结构为双向链表
Dequeue: 底层数据结如图所示,和vector单向开口且连续不同的是,Deque 双向开口,且分段连续。
关联容器:快速查找元素的容器,一般为O(log n)级别,底层数据结构均为红黑树
iterator遍历失效问题
- array/vector/deque这三种容器的内存空间连续,删除某一元素会使其后所有元素前移。(也即这些iterator都不准确了)
const和static修饰不同的地方作用
C++类的默认八种函数
class A
{
public:
// 默认构造函数;
A();
// 默认拷贝构造函数
A(const A&);
// 默认析构函数
~A();
// 默认重载赋值运算符函数
A& operator = (const A&);
// 默认重载取址运算符函数
A* operator & ();
// 默认重载取址运算符const函数
const A* operator & () const;
// 默认移动构造函数
A(A&&);
// 默认重载移动赋值操作符
A& operator = (const A&&);
};
空类的大小
在C++中空类会占一个字节,这是为了让对象的实例能够相互区别。具体来说,空类同样可以被实例化,并且每个实例在内存中都有独一无二的地址,因此,编译器会给空类隐含加上一个字节,这样空类实例化之后就会拥有独一无二的内存地址。如果没有这一个字节的占位,那么空类就无所谓实例化了,因为实例化的过程就是在内存中分配一块地址。
#include<iostream>
using namespace std;
class test
{
};
int main()
{
test a, b;
cout << "sizeof(test): " << sizeof(test) << endl;
cout << "addr of a: " << &a << endl; // 地址1
cout << "addr of b: " << &b << endl; // 地址2
system("pause");
return 0;
}
每个对象所占用的存储空间只是该对象的数据部分(虚函数指针和虚基类指针也属于数据部分)所占用的存储空间,而不包括函数代码所占用的存储空间。
- 所有类成员函数和非成员函数代码放在代码区
- 类的静态成员变量在类定义时就已经在全局数据区分配了内存,因而它是属于类的
- 非静态成员变量,我们是在类的实例化过程中(构造对象)才在栈区或者堆区为其分配内存,是为每个对象生成一个拷贝,所以它是属于对象的
值传递,引用传递
值传递:
形参是实参的拷贝,改变形参的值并不会影响外部实参的值。从被调用函数的角度来说,值传递是单向的(实参->形参),参数的值只能传入,不能传出。当函数内部需要修改参数,并且不希望这个改变影响调用者时,采用值传递。
指针传递:
形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作
const关键字
- const修饰普通类型的变量
常量,其值不可修改 - const修饰指针变量
const int *p = 8; // const修饰指针指向的内容,则内容为不可变量,值为8不可修改
int* cosnt p = &a; // const修饰指针,则指针为不可变量,内容可以修改
- const参数传递和函数返回值
参数:不可修改
返回值: 不可修改,不能作为左值,不能赋值,不可修改 - const修饰类成员函数
const 修饰类成员函数,其目的是防止成员函数修改被调用对象的值(类内的数据成员),不会调用其它非 const 成员函数,如果我们不想修改一个调用对象的值,所有的成员函数都应当声明为 const 成员函数。
注意:const 关键字不能与 static 关键字同时使用,因为 static 关键字修饰静态成员函数,静态成员函数不含有 this 指针,即不能实例化,const 成员函数必须具体到某一实例。
static关键字
类的静态成员变量在类实例化之前就已经存在了,并且分配了内存。函数的static变量在执行此函数时进行初始化。
- 全局静态变量:在全局变量前加上关键字static,全局变量就定义成一个全局静态变量.
-
- 内存中的位置:静态存储区,在整个程序运行期间一直存在。
-
- 初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化);·
-
- 作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到文件结尾。
- 局部静态变量:在局部变量之前加上关键字static,局部变量就成为一个局部静态变量。
-
- 内存中的位置:静态存储区
-
- 初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化);
-
- 作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变;
- 静态函数:在函数返回类型前加static,函数就定义为静态函数。函数的定义和声明在默认情况下都是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。函数的实现使用static修饰,那么这个函数只可在本cpp内使用,不会同其他cpp中的同名函数引起冲突;warning:不要再头文件中声明static的全局函数,不要在cpp内声明非static的全局函数,如果你要在多个cpp中复用该函数,就把它的声明提到头文件里去,否则cpp内部声明需加上static修饰;
- 类的静态成员:在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。对多个对象来说,静态数据成员只存储一处,供所有对象共用
- 类的静态函数:静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名。在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员(这点非常重要)。如果静态成员函数中要引用非静态成员时,可通过对象来引用。从中可看出,调用静态成员函数使用如下格式:<类名>::<静态成员函数名>(<参数表>);
new和malloc的区别
1、malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
2、对于非内部数据类型的对象而言,光用malloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。
3、由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。
4、C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。
5、new可以认为是malloc加构造函数的执行。new出来的指针是直接带类型信息的。而malloc返回的都是void指针。
malloc原理
malloc可以基于链表实现
1、它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表;
2、 调用malloc函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块。然后,将该内存块一分为二(一块的大小与用户请求的大小相等,另一块的大小就是剩下的字节)。接下来,将分配给用户的那块内存传给用户,并将剩下的那块(如果有的话)返回到连接表上。
3、 调用free函数时,它将用户释放的内存块连接到空闲链上。到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段,那么空闲链上可能没有可以满足用户要求的片段了。于是,malloc函数请求延时,并开始在空闲链上翻箱倒柜地检查各内存片段,对它们进行整理,将相邻的小空闲块合并成较大的内存块。
若分配内存小于 128k ,调用系统调用sbrk() ,将堆顶指针向高地址移动,获得新的虚存空间。
若分配内存大于 128k ,调用系统调用mmap() ,在文件映射区域中分配匿名虚存空间(堆和栈中间,称为文件映射区域的地方)。
空指针调用成员方法
- 如果调用编译器确定函数(普通成员函数、静态成员函数),该成员函数中需要对this指针指向的内容进行读取或者修改,出错;反之无错;
- 如果调用运行期确定函数(使用多态的虚函数),出错。
C++内存分配
在C++中,内存分成5个区
- 栈区(stack) 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
- 堆区(heap) 就是那些由 new 分配的内存块,一般一个 new 就要对应一个 delete。一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链。堆可以动态地扩展和收缩。
- 自由存储区 就是那些由 malloc 等分配的内存块,他和堆是十分相似的,不过它是用 free 来结束自己的生命的。
- 全局区(静态区)(static)全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域data段, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域bss段。程序结束后有系统释放
- 常量存储区 存放的是常量,不允许修改。常量字符串就是放在这里的。常量字符串不能修改, 否则程序会在运行期崩溃(当然,你要通过非正当手段也可以修改,而且方法很多).程序结束后由系统释放
程序代码区 存放函数体的二进制代码。
申请后系统的响应
- 栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
- 堆:操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时, 会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
栈:由系统自动分配,速度较快。但程序员是无法控制的。
堆:由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
void f()
{
int* p=new int[5];
//这条短短的一句话就包含了堆与栈,看到new,我们首先就应该想到,我们分配了一块堆内存,那么指针p呢?
//他分配的是一块栈内存,所以这句话的意思就是:在栈内存中存放了一个指向一块堆内存的指针p。
}
动态绑定
c++中,我们在使用基类的引用(指针)调用虚函数时,就会发生动态绑定。所谓动态绑定,就是在运行时,虚函数会根据绑定对象的实际类型,选择调用函数的版本。
纯虚函数
virtual void funtion1()=0;
定义一个函数为虚函数,不代表函数为不被实现的函数。
定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。
定义一个函数为纯虚函数,才代表函数没有被实现。
定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。
抽象类: 称带有纯虚函数的类为抽象类。
抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。
抽象类是不能定义对象的。
多继承产生的菱形继承问题
使用虚继承来解决
//间接基类A
class A{
protected:
int m_a;
};
//直接基类B
class B: virtual public A{ //虚继承
protected:
int m_b;
};
//直接基类C
class C: virtual public A{ //虚继承
protected:
int m_c;
};
//派生类D
class D: public B, public C{
public:
void seta(int a){ m_a = a; } //正确,不使用虚继承会命名冲突
// 因为类 B 和类 C 中都有成员变量 m_a(从 A 类继承而来),编译器不知道选用哪一个,所以产生了歧义。
为了解决多继承时的命名冲突和冗余数据问题,C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员。
void setb(int b){ m_b = b; } //正确
void setc(int c){ m_c = c; } //正确
void setd(int d){ m_d = d; } //正确
private:
int m_d;
};
int main(){
D d;
return 0;
}
函数模板与类模板有什么区别?
答:函数模板的实例化是由编译程序在处理函数调用时自动完成的,而类模板的实例化必须由程序员在程序中显式地指定。
cout < < MaxValue( 10 , 20) < < endl ; // 隐式调用
//MaxValue( 100 , 204) 这是一个模板函数
cout < < MaxValue< int > ( 10, 20) < < endl ; // 显式调用
Stack < int > intStack; //类模板的实例化由程序员显示的指定
模版特例化
可以特化类模板也可以特化函数模板,但类模板可以偏特化和全特化,而函数模板只能全特化。 模板实例化时会优先匹配"模板参数"最相符的那个特化版本。
模板机制为C++提供了泛型编程的方式,在减少代码冗余的同时仍然可以提供类型安全。 特化必须在同一命名空间下进行,可以特化类模板也可以特化函数模板,但类模板可以偏特化和全特化,而函数模板只能全特化。 模板实例化时会优先匹配"模板参数"最相符的那个特化版本。
C++的模板机制被证明是图灵完备的,即可以通过模板元编程(template meta programming)的方式在编译期做任何计算。
模板的声明
类模板和函数模板的声明方式是一样的,在类定义/模板定义之前声明模板参数列表。例如:
// 类模板
template <class T1, class T2>
class A{
T1 data1;
T2 data2;
};
// 函数模板
template <class T>
T max(const T lhs, const T rhs){
return lhs > rhs ? lhs : rhs;
}
// 全特化类模板
template <>
class A<int, double>{
int data1;
double data2;
};
// 函数模板
template <>
int max(const int lhs, const int rhs){
return lhs > rhs ? lhs : rhs;
}
template <class T2>
class A<int, T2>{
...
};
C++泛型编程
C++ 的泛型编程是基于模板实现的,而 C++ 的模板采用的是代码膨胀技术。例如 std::list 容器,如果你将 int 类型的数据存进去,C++ 编译器就为你生成一个专门用来存 int 类型数据的列表数据结构。也就是说,你向 std::list 容器中存放什么类型,C++ 编译器就为你生成相应的列表数据结构。理论上,数据的类型是无限的,因此 C++ 要生成的列表数据结构也是无限的。如果你的程序中有大量的数据类型要存到 std::list 容器,那么代码就会高度膨胀,这种膨胀是 C++ 编译器在目标文件连接阶段无法优化的。
C++ 内存泄漏
定义:在编写应用程序的时候,程序分配了一块内存,但已经不再持有引用这块内存的对象(通常是指针),虽然这些内存被分配出去,但是无法收回,将无法被其他的进程所使用,我们说这块内存泄漏了,被泄漏的内存将在整个程序声明周期内都不可使用。
原因:
- malloc后未free
- new后未delete
- 没有将基类的析构函数定义为虚函数,当基类的指针指向子类时,delete该对象时,不会调用子类的析构函数
- delet void * 的指针,导致没有调用到对象的析构函数,析构的所有清理工作都没有去执行从而导致内存的泄露;
Object* a = new Object(10, 'A');//Object*指针指向一个Object对象;
void* b = new Object(20, 'B');//void*指针指向一个Object对象;
delete a;//执行delete,编译器自动调用析构函数;
delete b;//执行delete,编译器不会调用析构函数,导致data占用内存没有得到回收;
常见解决办法:
(1)shared_ptr共享的智能指针:
shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。在最后一个shared_ptr析构的时候,内存才会被释放。
注意事项:
- 不要用一个原始指针初始化多个shared_ptr;
- 不要再函数实参中创建shared_ptr,在调用函数之前先定义以及初始化它;
- 不要将this指针作为shared_ptr返回出来;
- 要避免循环引用。
(2)unique_ptr独占的智能指针:
- unique_ptr是一个独占的智能指针,他不允许其他的智能指针共享其内部的指针,不允许通过赋值将一个unique_ptr赋值给另外一个 unique_ptr;
- unique_ptr不允许复制,但可以通过函数返回给其他的unique_ptr,还可以通过std::move来转移到其他的unique_ptr,这样它本身就不再 拥有原来指针的所有权了;
- 如果希望只有一个智能指针管理资源或管理数组就用unique_ptr,如果希望多个智能指针管理同一个资源就用shared_ptr
如何排查内存泄漏
valgrind工具
gcc 编译时带上-g选项
valgrind --tool=memcheck --leak-check=full ./main_c
Valgrind 也会报告程序是在哪个位置发生内存泄漏
delete指针
int* p = new int(1);
delete p; // 删除指针所指向的目标(变量或对象),释放堆空间,并不是删除p本身
// 指针p的真正释放是随着函数调用的结束而消失,释放堆空间后,p成了"空指针"。
// 如果我们在delete p后没有进行指针p的制空(p=NULL)的话,
// 其实指针p这时会成为野指针,
// 为了使用的安全,我们一般在delete p之后还会加上p=NULL这一语句
delete : 用来释放new分配的单个对象指针指向的内存。
delete[] : 用来释放new分配的对象数组指针指向的内存。
delete只会调用一次析构函数,而delete[]会调用每一个成员函数的析构函数。
C++ 11新特性
- auto类型推导:让编译器在编译期就推导出变量的类型,可以通过=右边的类型推导出变量的类型
- 列表初始化
- lambda匿名函数
- 三种智能指针
智能指针都是类模版的方式实现的
std::shared_ptr<int> p1 = std::make_shared<int>(25);
// std::make_shared<int>(25) 堆内存的计数器
cout << p1.use_count() <<endl; // 1
{
std::shared_ptr<int> p2 = p1;
cout << p1.use_count() <<endl; // 2
}
cout << p1.use_count() <<endl; // 1
std::unique_ptr<int> p1 = std::make_unique<int>(25);
std::unique_ptr<int> p2 = p1; // error
//unique_ptr的“独占”是指:不允许其他的智能指针共享其内部的指针,不允许通过赋值将一个unique_ptr赋值给另一个unique_ptr。
std::unique_ptr<int> p3 = std::move(p1); // ok
// 可以通过move转移所有权到其他unique_ptr,这时p1为空,不可访问。
- for基于范围的循环for(😃
- nullptr:总之在 C++11 标准下,相比 NULL 和 0,使用 nullptr 初始化空指针可以令我们编写的程序更加健壮。
常用数据结构
vector<int> nums;
C++四种强制类型转换
const_cast , static_cast , dynamic_cast , reinterpret_cast
C风格的类型转换
TypeName b = (TypeName)a;
-
const_cast1、
常量指针被转化成非常量的指针,并且仍然指向原来的对象;
常量引用被转换成非常量的引用,并且仍然指向原来的对象;
const_cast一般用于修改指针。如const char *p形式 -
static_cast
类似于C风格转换 -
dynamic_cast
父类转子类,子类转父类 -
reinterpret_cast
const_cast:用于将const变量转为非const
static_cast:用于各种隐式转换,比如非const转const,void*转指针等, static_cast能用于多态向上转化,如果向下转能成功但是不安全,结果未知;
dynamic_cast:用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。
向上转换:指的是子类向基类的转换
向下转换:指的是基类向子类的转换
reinterpret_cast:几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;
string类型的局部变量不能通过变量名作为函数返回值返回
/*
string half(string s)
{
int n = s.length() / 2;
string str;
for (int i = 0; i < n; i++)
{
str[i] = s[i];
}
return str;//返回str,相当于返回一个指针。str指向string类型的变量。
}
*/
// string类型的局部变量不能通过变量名作为函数返回值返回
// 因为局部变量是存放在栈区,栈区的数据在函数执行后会自动释放。
// 从而string类型的局部变量也就会被释放。
// str虽然作为指针被返回了,但是它所指向的区域中的内容也已经被释放了。
// 解决方法是:通过new开辟空间,将指针所指向的数据放到堆区
string half(string s)
{
int n = s.length() / 2;
char* str = new char[n+1];
for (int i= 0; i < n; i++)
{
str[i] = s[i];
}
str[n] = '\0';
return str;
}
static char p[] = "this is test0\n";
return p; //保存在静态存储区, 正常
char *p = "this is test1\n";
return p; // 保存在只读常量区,不在通常所说的栈内存,正常
public protected private
修饰类的成员
- 公有(public)成员
公有成员在程序中类的外部是可访问的 - 私有(private)成员
私有成员变量或函数在类的外部是不可访问的,甚至是不可查看的。只有类和友元函数可以访问私有成员 - protected(受保护)成员
protected(受保护)成员变量或函数与私有成员十分相似,但有一点不同,protected(受保护)成员在派生类(即子类)中是可访问的。
继承的特点
有public, protected, private三种继承方式,它们相应地改变了基类成员的访问属性。
-
public 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:public, protected, private
-
protected 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:protected, protected, private
-
private 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:private, private, private
但无论哪种继承方式,上面两点都没有改变:
- private 成员只能被本类成员(类内)和友元访问,不能被派生类访问;
- protected 成员可以被派生类访问。
指针和引用的区别
- 指针有自己的一块空间,而引用只是一个别名;
- 使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;
- 指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象 的引用;
- 作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引 用的修改都会改变引用所指向的对象;
- 可以有const指针,但是没有const引用;
- 指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能 被改变;
- 指针可以有多级指针(**p),而引用止于一级;
- 指针和引用使用++运算符的意义不一样;
- 如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。
浅拷贝与深拷贝
总结:浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。
C++源文件到可执行文件经历的过程
对于C/C++编写的程序,从源代码到可执行文件,一般经过下面四个步骤:
1).预处理,产生.ii文件
2).编译,产生汇编文件(.s文件)
3).汇编,产生目标文件(.o或.obj文件)
4).链接,产生可执行文件(.out或.exe文件)
C++ 多线程
互斥:具有唯一性和排他性,没有限制先后次序
同步:同步已经实现互斥,有严格的先后次序
mutex.lock() mutex.unlock()
lock_guard
ock_guard是一个互斥量包装程序,它提供了一种方便的RAII(Resource acquisition is initialization )风格的机制来在作用域块的持续时间内拥有一个互斥量。
创建lock_guard对象时,它将尝试获取提供给它的互斥锁的所有权。当控制流离开lock_guard对象的作用域时,lock_guard析构并释放互斥量。
它的特点如下:
- 创建即加锁,作用域结束自动析构并解锁,无需手工解锁
- 不能中途解锁,必须等作用域结束才解锁
- 不能复制
std::lock_guardstd::mutex lock(my_mutex);
unique_lock
- 创建时可以不锁定(通过指定第二个参数为std::defer_lock),而在需要时再锁定
- 可以随时加锁解锁
- 作用域规则同 lock_grard,析构时自动释放锁
- 不可复制,可移动
- 条件变量需要该类型的锁作为参数(此时必须使用unique_lock)
inline函数
相当于把内联函数里面的内容直接写在调用内联函数处;
相当于不用执行进入函数的步骤,直接执行函数体。
内联函数同宏函数一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收,结果返回等,从而提高程序运行速度。
缺点:
代码膨胀。内联是以代码膨胀(复制)为代价,消除函数调用带来的开销。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
虚函数可以是内联函数吗?
虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
内联是在编译期建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
如何定义一个只能在堆上(栈上)生成对象的类?
只能在堆上
方法:将析构函数设置为私有
原因:C++ 是静态绑定语言,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性。若析构函数不可访问,则不能在栈上创建对象。
ps: 只能定义其他函数释放空间,delete this
只能在栈上
方法:将 new 和 delete 重载为私有
原因:在堆上生成对象,使用 new 关键词操作,其过程分为两阶段:第一阶段,使用 new 在堆上寻找可用内存,分配给对象;第二阶段,调用构造函数生成对象。将 new 操作设置为私有,那么第一阶段就无法完成,就不能够在堆上生成对象。
std::vector vec = {1, 2, 3};
对象cec是在栈上创建的。但是,STL 的vector类其实是在堆上面存储数据的(这点可以查看源代码)。因此,只有对象v本身是在栈上的,它所管理的数据(这些数据大多数时候都会远大于其本身的大小)还是保存在堆上。
左值,右值,左值引用,右值引用
左值:指存在于单个表达式之外的对象。你可以把左值当成有名字的对象。
右值:是一个暂时存在的值存在于单个表达式之内的对象。
int x = 3 + 4; // 很显然,x是左值,3 + 4是右值。
通俗来说就是,左值的生存期不只是这句话,后面还能用到它。
而右值呢,出了这句话就挂了,就是临时对象,没有名字,所以也叫将亡值。
左值引用:
int a = 10;
int &b = a; // 定义一个左值引用变量
b = 20; // 通过左值引用修改引用内存的值
左值引用在汇编层面其实和普通的指针是一样的;定义引用变量必须初始化,因为引用其实就是一个别名,需要告诉编译器定义的是谁的引用。
int &var = 10;
上述代码是无法编译通过的,因为10无法进行取地址操作,无法对一个立即数取地址,因为立即数并没有在内存中存储,而是存储在寄存器中,可以通过下述方法解决:
const int &var = 10;
使用常引用来引用常量数字10,因为此刻内存上产生了临时变量保存了10,这个临时变量是可以进行取地址操作的,因此var引用的其实是这个临时变量,相当于下面的操作:
const int temp = 10;
const int &var = temp;
根据上述分析,得出如下结论:
左值引用要求右边的值必须能够取地址,如果无法取地址,可以用常引用;
但使用常引用后,我们只能通过引用来读取数据,无法去修改数据,因为其被const修饰成常量引用了。
那么C11 引入了右值引用的概念,使用右值引用能够很好的解决这个问题。
右值引用:
右值引用是C 11新增的特性,所以C++ 98的引用为左值引用。右值引用用来绑定到右值,绑定到右值以后本来会被销毁的右值的生存期会延长至与绑定到它的右值引用的生存期。
类型 && 引用名 = 右值表达式;
int &&var = 10;
在汇编层面右值引用做的事情和常引用是相同的,即产生临时量来存储常量。但是,唯一 一点的区别是,右值引用可以进行读写操作,而常引用只能进行读操作。
右值引用的存在并不是为了取代左值引用,而是充分利用右值(特别是临时对象)的构造来减少对象构造和析构操作以达到提高效率的目的。
函数指针:
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
eg: 使用函数指针作为函数参数来实现四则运算,
设计如下函数
int calculate(int a, int b, fun_t operation)
{
int result;
result = operation(a, b); // 运算
return result;
}
fun_t是一个函数指针,其定义为:
typedef int (*fun_t)(int, int);
该函数指针fun_t指向一个带两个int类型的形参、int类型的返回值的函数。使用关键字typedef对int (*)(int, int)进行重命名(封装)为fun_t。关于typedef与define的区别可查看往期笔记:【C语言笔记】#define与typedef的区别?
根据函数指针变量operation指向不同的运算函数可实现加法运算、减法运算、乘法运算、除法运算。
主函数代码如下:
int main(void)
{
int result;
int a = 192, b = 48;
/* 两个数相加的操作 */
result = calculate(a, b, add2);
printf("加法运算: %d+%d = %d\n",a, b, result);
/* 两个数相减的操作 */
result = calculate(a, b, sub2);
printf("减法运算: %d-%d = %d\n",a, b, result);
/* 两个数相乘的操作 */
result = calculate(a, b, mul2);
printf("乘法运算: %d*%d = %d\n",a, b, result);
/* 两个数相除的操作 */
result = calculate(a, b, div2);
printf("除法运算: %d/%d = %d\n",a, b, result);
return 0;
}
实现运算的4个函数很简单,如下:
int add2(int a, int b)
{
return a+b;
}
int sub2(int a, int b)
{
return a-b;
}
int mul2(int a, int b)
{
return a*b;
}
int div2(int a, int b)
{
return a/b;
}
typedef void DRAWF( int, int );
DRAWF box;
等价于
void box( int, int );
多态
#include <iostream>
using namespace std;
class A {
public:
void test() {
cout << "test in base class" << endl;
func1();
}
void func1() {
cout << "func1 in base calss"<<endl;
}
};
class B : public A {
public:
void func1() {
cout << "func1 in derived calss"<<endl;
}
};
int main() {
A* b = new B();
b->test();
return 0;
}
/* 结果:
test in base class
func1 in derived calss
没加virtual关键字不会调用B::func1
导致错误输出的原因是,调用函数 area() 被编译器设置为基类中的版本,这就是所谓的静态多态,或静态链接 - 函数调用在程序执行前就准备好了。
有时候这也被称为早绑定,因为 area() 函数在程序编译期间就已经设置好了。
需要加上virtual 关键字!
虚函数 是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。
我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。
存在虚函数的类都有一个一维的虚函数表叫做虚表。类的对象有一个指向虚表开始的虚指针。
*/
虚表是和类对应的,虚表指针是和对象对应的
char和char&的区别
// 如果不改变地址,两个传参一模一样
funcA(char* A) {
A->a = 10;
}
funcB(char*& A) {
A->a = 10;
}
// A地址为001
funcA(char* A) {
// A地址改为002
}
funcB(char*& A) {
// A地址改为002
}
// 只有funcB的指针地址可以修改为002, funcA还是001
// 类似于指针的引用
返回局部变量
X bar() {
X xx;
// 处理xx
return xx;
}
'''
(把局部对象拷贝出来了)
'''
// 返回值如何从局部对象xx中拷贝过来?
1. 加上一个额外参数,类型是class object的一个reference
2. return前插入一个copy constructor调用操作
// 函数转换
void bar(X& _result) {
X xx;
// 编译器产生的default constructor调用
xx.X::X();
//处理xx
// 编译器产生的copy constructor调用
_result.X::X(xx);
return; // 不返回任何值
}
成员初始化列表
class Word {
String _name;
int _cnt;
}
Word::Word() {
_name = 0;
_cnt = 0;
}
// 编译器伪代码
Word::Word() {
// 调用String的default constructor
_name.String::String();
// 生成临时对象
String temp = String(0);
// memberwise 地拷贝(赋值操作符)_name;
_name.String::operator=(temp);
//删除临时对象
tmp.String::~String();
_cnt = 0;
}
Word::Word : _name(0) {
_cnt = 0;
}
// 编译器处理
Word::Word() {
// 调用String(int) constructor
_name.String::String(0);
_cnt = 0;
}
评论区