title: 面向对象的三大特性-类的访问权限-new/mdelete与malloc/free-虚函数机制是如何被激活-如何证明虚函数表存在,派生类对象指针指向基类- 纯虚函数替换虚函数-STL空间配置器-map/unordered_map-set/unordered_set-移动语义-右值-C++与C区别
date: 2022-02-18 11:00:52
tags: Cplusplus
toc: true
参考网址:https://gitee.com/NiceBlueChai/CPlusPlusThings/tree/master/basic_content
1、面向对象的三大特性:封装、继承、多态(还有一种说法是四大特性,包括抽象)
- 封装:对实体的属性和功能进行访问控制,向信任的实体开放,对不信任的实体异常。通过开放的外部接口即可访问,无需知道功能如何实现。一般用到private关键字。
- 继承:低层级的类可以沿用高层级类的特征和方法。为多态打下基础。从既有类产生新类。
- 多态:一个类的同名方法,在不同情况下的实现细节不同。
封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类)。它们的 目的都是为了代码重用。而多态除了代码的复用性外,还可以解决项目中紧偶合的问题,提高程序的可 扩展性。
2、类的访问权限:private、protected、public
- public:可以被该类中的函数、子类的函数、友元函数访问,也可以由该类的对象访问;公开都可以访问。
- protected:可以被该类中的函数、子类的函数、友元函数访问,但不可以由该类的对象访问;类对象不可以访问。
- private:可以被该类中的函数、友元函数访问,但不可以由子类的函数、该类的对象、访问。更好地隐藏类的内部实现。
如果不写清楚成员的访问权限,成员默认private。
3、类的构造函数、析构函数、赋值函数、拷贝函数
3.1 构造函数
分为有参构造,无参构造。没有返回值。创建一个新的对象时候给数据成员赋初值。
常用写法:
//这种类外写法涵盖了无参,有参,十分实用
Point::Point(int ix=0,int iy=0,int iz=0)
:_ix(ix)
,_iy(iy)
,_iz(iz)
{}
3.2 析构函数
没有返回值,没有参数,也没有初始化。销毁对象时候调用析构函数来释放内存,避免内存泄漏。
常用写法:
String::~String()
{
delete [] _ptr;
}
3.3 赋值函数
赋值和构造区别
赋值和拷贝的区别是,赋值是将已存在对象赋予给新建对象,拷贝是将已存在对象拷贝给已存在对象。
String &String::operator=(const String &rhs)
{ //赋值
cout << "operator=(const String &)" << endl;
if (this != &rhs) //判断是否自复制
{
delete[] _pstr;
_pstr = new char[strlen(rhs._pstr) + 1]();
strcpy(_pstr, rhs._pstr);
}
return *this;
}
3.4 拷贝函数
拷贝构造函数也分为深拷贝和浅拷贝。
String::String(const String &rhs)
: _pstr(new char[strlen(rhs._pstr) + 1]()) //深拷贝
{
strcpy(_pstr, rhs._pstr);
cout << "String(const String &)" << endl;
}
4、移动构造函数与拷贝构造函数对比
移动构造函数是c++11的新特性,移动构造函数传入的参数是一个右值 用&&标出。
左值可以通过使用std:move方法强制转换为右值。
首先讲讲拷贝构造函数:拷贝构造函数是先将传入的参数对象进行一次深拷贝,再传给新对象。这就会有一次拷贝对象的开销,并且进行了深拷贝,就需要给对象分配地址空间。而移动构造函数就是为了解决这个拷贝开销而产生的。移动构造函数首先将传递参数的内存地址空间接管,然后将内部所有指针设置为nullptr,并且在原地址上进行新对象的构造,最后调用原对象的的析构函数,这样做既不会产生额外的拷贝开销,也不会给新对象分配内存空间。
而对于指针参数来讲,需要注意的是,移动构造函数是对传递参数进行一次浅拷贝。
也就是说如果参数为指针变量,进行拷贝之后将会有两个指针指向同一地址空间,这个时候如果前一个指针对象进行了析构,则后一个指针将会变成野指针,从而引发错误。
所以当变量是指针的时候,要将指针置为空,这样在调用析构函数的时候会进行判断指针是否为空,如果为空则不回收指针的地址空间,这样就不会释放掉前一个指针。
1.左值引用:常规的引用,在汇编层,左值引用与指针是一样的,定义引用变量必须初始化,引用仅是一个别名,仍指向所引用的空间。所以,左值引用的必须是能够取址的,若不能取址(如立即数),可使用常引用。
2.右值引用:用来绑定到右值,绑定到右值(不能取地址的,没有名字的,临时的就是右值)以后本来会被销毁的右值的生存期会延长至与绑定到它的右值引用的生存期。
5、深拷贝与浅拷贝的区别
//浅拷贝
Computer::Computer(const Computer &com1)
:_price(com1._price)
,_brand(com1._brand)//这里拷贝构造函数的_brand还是指向com1._brand的堆空间
{
cout<<"Computer(const Computer &)"<<endl;
}
//深拷贝
Computer::Computer(const Computer &com1)
:_price(com1._price)
,_brand(new char[strlen(com1._brand)+1]())
{
strcpy(_brand,com1._brand);
cout<<"Computer(const Computer &)"<<endl;
}
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
6、空类有哪些函数?空类的大小?
https://blog.csdn.net/liuweiyuxiang/article/details/89263181
7、内存分区:全局区、堆区、栈区、常量区、代码区
https://www.jianshu.com/p/b2f9c7758495
8、C++与C的区别
主要区别:c语句是面向结构的语言,c++是面向对象的语言,C++从根本上已经发生质飞跃,并对c进行丰富的扩展。
联系:c是c++的子集,所以大部c语言程序都可以不加修改的拿到c++下使用。
注意事项
- C++虽然主要是以C的基础发展起来的一门新语言,但她不是C的替代品,不是C的升级,C++和C是兄弟关系。
- 记住唯一适合学习的编译器是gcc/mingw。
- 不要用""代替<>来包含系统头文件。
- 不要将main函数的返回类型定义为void。
- 请用int main( int argc, char *argv[ ] )
int main(int argc,char **argv)
- 不要用#include <iostream.h>
#include <iostream> //取代#include <iostream.h>
#include <cstring> //取代#include <string.h>
如果这个头文件是旧C++特有的,那么去掉。h后缀,并放入std名字空间,
如果这个头文件是C也有的,那么去掉。h后缀,增加一个c前缀,比如 string.h
9、struct与class的区别
- 默认的访问权限。结构体默认成员访问权限为public,而class默认成员访问权限为private
- class花括号最后要有分号, 而struct不用
- 默认的继承权限,class默认的是private,strcut默认的是public
10、struct内存对齐
内存对齐主要遵循下面三个原则:
- 结构体变量的起始地址能够被其最宽的成员大小整除
- 结构体每个成员相对于起始地址的偏移能够被其自身大小整除,如果不能则在前一个成员后面补充字节
- 结构体总体大小能够被最宽的成员的大小整除,如不能则在后面补充字节
11、new/delete与malloc/free的区别
https://cloud.tencent.com/developer/article/1792158#:~:text=malloc%2Ffree%E6%98%AFC%2B%2B%2F,%E5%BF%85%E9%A1%BB%E6%89%A7%E8%A1%8C%E6%9E%90%E6%9E%84%E5%87%BD%E6%95%B0%E3%80%82
new=malloc+ 构造函数
delete=free+析构函数
12、内存泄露的情况
https://www.cnblogs.com/SeekHit/p/6549940.html#commentform
13、sizeof与strlen对比
https://www.runoob.com/note/27755
14、指针与引用的区别
https://blog.csdn.net/weikangc/article/details/49762929#:~:text=%E6%8C%87%E9%92%88%E4%BB%8E%E6%9C%AC%E8%B4%A8%E4%B8%8A%E8%AE%B2,%E4%BE%9D%E9%99%84%E4%BA%8E%E5%90%8C%E4%B8%80%E4%B8%AA%E5%8F%98%E9%87%8F%EF%BC%89%E3%80%82
15、野指针产生与避免
https://www.cnblogs.com/aiden-zhang/p/11406400.html
17、虚函数是什么
https://gitee.com/NiceBlueChai/CPlusPlusThings/tree/master/basic_content/virtual
虚函数就是在基类中被声明为virtual,并在一个或多个派生类中被重新定义的成员函数。
// 类内部
class 类名 {
virtual 返回类型 函数名(参数表)
{
//...
}
};
//类之外
virtual 返回类型 类名::函数名(参数表)
{
//...
}
如果一个基类的成员函数定义为虚函数,那么它在所有派生类中也保持为虚函数,即使在派生类中省略了virtual关键字,也仍然是虚函数。派生类要对虚函数进行中可根据需重定义,
重定义的格式有一定的要求:
- 与基类的虚函数有相同的参数个数;
- 与基类的虚函数有相同的参数类型;
- 与基类的虚函数有相同的返回类型。
如何证明虚函数表存在?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zdNnBsTJ-1663205777327)(7.jpg)]
void test()
{
//虚表只有一张,位于只读段,见上图
Derived derived1(10, 20);
printf("derived1对象的地址: %p\n", &derived1);//取对象的地址作为指针,对象地址开头就是虚表指针
printf("derived1对象的地址: %p\n", (long *)&derived1);//&derived1=&vfptr
printf("虚表的地址: %p\n", (long *)*(long *)&derived1);//如果虚表存在,对象地址对应的数值是一个指针,指向虚表,进行解引用之后得到虚表的地址,再进行转格式显示
//vfptr=*&derived1
printf("虚函数的地址: %p\n", (long *) * (long *)*(long *)&derived1);//对虚表的地址开头解引用,返回一个指针,就是虚表存储的第一个虚函数的地址
//*vfptr =**&derived1)=&Derived::Display()=tmp
cout << endl;
typedef void (*pFunction)(void);//函数的类型,返回值为void 参数为void
pFunction tmp = (pFunction)*((long *)*(long *)&derived1); //函数指针 tmp()=Derived::Display()
tmp(); //程序代码区 虚函数实现
printf("虚函数的地址: %p\n", tmp);//tmp=
tmp = (pFunction)*((long *)*(long *)&derived1 + 2);
tmp();
printf("第三个虚函数的地址: %p\n", tmp);
}
当一个基类中设有虚函数,而一个派生类继承了该基类,并对虚函数进行了重定义,我们称之为覆盖 (override). 这里的覆盖指的是派生类的虚函数表中相应虚函数的入口地址被覆盖。
虚函数机制是如何被激活的呢,或者说动态多态是怎么表现出来的呢?从上面的例子,可以得出结论:
- 基类定义虚函数——实现虚函数
- 派生类重定义(覆盖、重写)虚函数——派生类重写才有意义
- 创建派生类对象——体现多态的多
- 基类的指针指向派生类对象——多态是不同派生类对象对来自基类的同一信息执行不同的行为,而接受同一信息的实现就是用基类指针。
class Shape {
public:
virtual double area() const = 0; //纯虚函数
};
class Square
: public Shape
{
public:
Square(double s) {
size = s;
}
virtual double area() const {
return size * size;
}
private:
double size;
};
class Circle
: public Shape {
public:
Circle(double r) {
radius = r;
}
virtual double area() const {
return 3.14159 * radius * radius;
}
private:
double radius;
};
int main()
{
Shape* array[2]; //定义基类指针数组
Square Sq(2.0);
Circle Cir(1.0);
array[0] = &Sq;
array[1] = &Cir;
for (int i = 0; i < 2; i++) /
{
cout << array[i]->area() << endl;
}
return 0;
}
为什么不能反过来,派生类对象指针指向基类?
当基类的指针(P)指向派生类的时候,只能操作派生类中从基类中继承过来的数据和基类自身的数据。
而派生类指向基类的指针,因为内存空间比基类长,会导致严重的后果,所以不允许派生类的指针指向基类,虽然通过强制转换是可以编译通过的,但这是极不安全的做法。
- 基类指针调用虚函数
含有虚函数的基类对象和派生类对象都有vptr指针,每个类的vptr指针指向相应的虚函数表。基类指针(或引用)指向派生类对象(或引用)时调用虚函数时,派生类vptr指针找到虚函数表,根据虚函数表找到相应虚函数的入口地址,然后进行调用。这是动态联编。
为什么要用纯虚函数替换虚函数?
纯虚函数是一种特殊的虚函数,在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。这就是纯虚函数的作用。
20、static关键字:修饰局部变量、全局变量、类中成员变量、类中成员函数
https://gitee.com/NiceBlueChai/CPlusPlusThings/tree/master/basic_content/static
22、extern关键字:修饰全局变量
关键字extern用来声明变量或者函数是一个外部变量或者外部函数,也就是说告诉编译器是在其他文件中定义的,编译的时候不要报错,在链接的时候按照字符串寻址可以找到这个变量或者函数。(函数默认是外部类型的,不需要显示声明,但是变量必须,如果想把一个函数声明为只在本文件范围有效,那么可以用static来说明)
https://gitee.com/NiceBlueChai/CPlusPlusThings/tree/master/basic_content/extern
23、volatile关键字:避免编译器指令优化
https://gitee.com/NiceBlueChai/CPlusPlusThings/tree/master/basic_content/volatile
因为编译器会自动优化,如果不用这个关键字告诉编译器不要进行优化,会出现很多逻辑问题,达不到你的要求。
24、四种类型转换:static_cast、dynamic_cast、const_cast、reinterpret_cast
25、右值引用,std::move函数
https://zhuanlan.zhihu.com/p/335994370
27、四种智能指针及底层实现:auto_ptr、unique_ptr、shared_ptr、weak_ptr
https://zhuanlan.zhihu.com/p/64543967
28、shared_ptr中的循环引用怎么解决?(weak_ptr)
https://blog.csdn.net/lijinqi1987/article/details/79005738
29、vector与list比较
https://blog.csdn.net/yu876876/article/details/81698269
30、vector迭代器失效的情况
https://www.cnblogs.com/linuxAndMcu/p/14621819.html
31、map与unordered_map对比
https://xzchsia.github.io/2020/04/09/cpp-map-unordered_map/#:~:text=%E4%B8%8D%E5%90%8C%E7%9A%84%E6%98%AFunordered_map%E4%B8%8D,%E4%BC%9A%E5%BE%97%E5%88%B0%E6%9C%89%E5%BA%8F%E9%81%8D%E5%8E%86%E3%80%82
32、set与unordered_set对比
https://www.cnblogs.com/codingmengmeng/p/13992692.html#:~:text=1%E3%80%81set%E6%AF%94unordered_set%E4%BD%BF%E7%94%A8,%E5%A4%8D%E6%9D%82%E5%BA%A6(%E4%BE%8B%E5%A6%82insert)%E3%80%82