C/C++
20200703
-
编译器默认提供 默认构造函数、析构函数、默认拷贝构造函数
-
如果自定义了有参构造函数,编译器不再提供默认构造函数,需要自己写,仍然提供默认拷贝构造函数
-
如果自动以了拷贝构造函数,编译器 同样不再提供默认拷贝构造函数、也不提供默认构造函数
-
构造函数的分类:
①默认构造函数:编译器默认会实现一个空的,啥也不干
②带参构造函数:自己实现;如果定义,则编译器不会再默认生成默认构造函数,也就是我们最好自己定义一个无参的默认构造函数;
③拷贝构造函数:编译器会默认实现一个,只拷贝值(是浅拷贝),我们最好自己实现一个能够深拷贝的,如果定义,编译器不会再生成默认构造函数,因此我们最好也实现一个默认构造函数;
注:Person(const Person& p){}
-
拷贝构造函数的调用:
①:使用一个已经创建的对象初始化另一个对象;
②:值传递方式给函数参数传值
③:以值方式返回局部对象; -
深拷贝浅拷贝:如果属性有在堆区申请的,一定要自己写一个拷贝构造函数完成深拷贝;
错误示例:
修改一下:
-
静态成员变量与静态成员函数
①:静态成员变量:
类中的静态成员变量被所有本类对象共享;
在编译的时候分配空间在全局区;
只能在类外进行初始化!!!
②:静态成员函数:
所有对象共享一个函数;
静态成员函数只能访问静态成员变量!!!(因为没有this指针) -
C++类成员存储:记住一句话:只有非静态成员变量,才属于类上对象
换言之:静态成员变量、成员函数、静态成员函数、所有的函数!!都不占用类对象的空间;
但是 空类的对象,占用1个字节的大小;为了存储地址,标识不同的类对象地址 -
this指针:指向被调用成员函数所属的对象
this是隐含在每一个非静态成员函数中的一个指针 -
this指针的用途:
①:形参和成员变量同名,可以用this指针区分(最好还是命名规范些)
②: 链式编程:非静态成员函数中返回对象本身!return *this
;
下图中:p2对象可以连续调用add函数,因为返回是*this
对象本身的引用
返回值类型为:person&, 因此一直返回的是p2这个对象;因此p2.m_age = 30;
若返回值类型改为person
会发生什么?p2.m_age == 20
因为返回一个值,会调用拷贝构造函数,p2.addAge(p1)
返回一个无名对象,这个对象再调用add函数,又返回另一个无名对象,重复这过程,最后这句话执行完,所有无名对象析构! p2只add了第一次;
- 空指针访问成员函数:
C++中允许空指针访问成员函数!但不能使用this指针,因为this指针指向的是:调用函数的对象本身,而空指针又是空的。。。所以不行!!!!的
下方的showstr可以执行,但是showage不行,因为在这个函数中使用了this指针! 也就是访问了成员变量;
- const修饰成员函数与const修饰常对象
①常函数:
1)成员函数后面+const,称之为常函数
2)常函数内不可修改成员属性;
3)成员属性声明时+mutable关键字,可以在常函数中被修改;
原理:函数后面+const表示对于this指针的定义:
原本this指针类型为Person* const this 指针常量:指针本身不可以被修改
+const后为: const Person * const this:指针本身和指针指向的值都不可以被修改!!!!
②:常对象
这个就与变量很像了,就是在前面加上const关键字就ok
常对象只能调用常函数(其实讲道理,只要不在函数中使用this指针去修改变量的值,也就是不修改成员变量,不是常函数不是应该也可以吗??但是!!就是不行,为了防止意外!常对象只能调用常函数!配套!!!)
-
友元三种实现:全局函数做友元、友元类、成员函数做友元
①: 全局函数做友元:
②: 友元类,类做友元
一个类A无法访问类B中的私有成员数据;
只需要将类A,在类B中添加声明:friend class A;
即可;这样类A的所有成员函数中都可以访问类B的成员了;
③:成员函数做友元
一个类A无法访问类B中的私有成员数据;现在想让A中某个函数void visit()
能够访问类B中的私有成员:
只需要将类A中的成员函数,在类B中添加声明:friend void A::visit();
即可;这样类A的visit成员函数中可以访问类B的私有成员了; -
重载 << 运算符时,只能使用全局函数来进行重载
为何不能使用成员函数重载呢?
使用成员阿函数重载void operator<<(person& p)?????
这样肯定是不对的,这样的调用情况相当于:Person p1; p1.operator<<(p);
对应 p1 << p;这是啥啊啊???
那Person p1; cout.operator<<(p1);
对应了 cout << p1;
这不就ok了嘛!!!!!但是成员函数做不到让cout调用成员函数啊~~~~
所以,使用全局的就很容易了:void operator<<(ostream& cout , person& p);
为了实现链式编程思想:ostream& operator<<(ostream& cout, person& p);
搞定!
-
重载++运算符(前置和后置)(要使用成员函数重载)
1) 前置++运算符重载,入参为void
,需要注意返回引用,因为涉及到连续调用问题,需要是同一个对象;
2) 后置++运算符重载,入参为int
,这个是占位符,如果使用别的会报错如下图;需要返回值而不是引用,在重载函数内部将对象相应变量++,记录++之前的数值,返回一个+=之前的*this;(但是无法解决连续调用问题,因为返回的是值不是引用)
3) 我们需要提前对 << 运算符完成重载,不然无法使用cout;注意,VSCODE中,在 << 与++碰到一起,编译器优化问题,需要对 << 运算符重载时,将第二个变量声明为const;
注意:重载后置++必须使用int作为入参,占位
-
重载=运算符
注意:链式编程思想,返回值为*this 引用
1) 编译器会默认提供赋值函数,里面的实现时浅拷贝,我们需要完成深拷贝的实现;
2)这类似于自定义的拷贝构造函数实现,需要我们对当前指针的判空,与堆空间的释放;
-
3种继承方式权限:
-
继承后子类对象大小:
子类对象包含父类中的全部非静态成员属性,就算是private的,不能被子类访问,sizeof(子类对象)也会包含这些成员;这是被编译器隐藏了,因此访问不到,但是有 -
子类中访问父类的同名成员与属性
1)子类对象可以直接访问到子类中同名成员
2)子类对象加作用域可以访问到父类同名成员
3)当子类与父类拥有同名的函数成员,子类中会隐藏父类中的同名成员函数,加作用域可以访问到父类中的同名函数
- 子类访问父类的静态成员变量时,其实这份变量只有一份,修改子类的和父类的都会改变这个共有的变量,其地址是一样的;
class Base{
public:
Base(){ m_A = 100;}
void fun(){ cout << " Base中 fun()" << endl;
void fun(int x){ cout << " Base中 fun(int x )" << endl;
int m_A;
};
class Son : public Base{
Son(){ m_A = 200;}
void fun(){ cout << " Son 中 fun()" << endl;
int m_A;
}
void test(){
Son son;
cout << son.m_A << endl; // 200
cout << son.Base::m_A << endl; // 100
son.fun(); // Son 中 fun()
son.Base::fun(); //Base中 fun()
son.Base::fun(3); // Base中 fun(int x )
}
-
子类中访问父类的同名、静态、成员与函数
与上面的非静态访问方式一致;
只不过父类中的函数可以通过 Base::fun();也可以Son::Base::fun();
别忘了,静态成员变量类内声明、类外初始化;
静态成员函数类内外初始化都可,但只能访问静态成员变量(因为没有this指针) -
菱形继承 &&虚继承方法
问题:
1)羊继承了动物的数据,驼也继承了动物,羊驼使用数据时,会产生二义性。
2)羊驼 继承自动物的数据被复制了两份,如何访问-》使用作用域符号访问,但其实这份数据属于冗余,我们只要一份就好了;
3)使用虚继承方法:羊和驼分别使用虚继承方法继承动物类(虚继承时称为虚基类)
总结:
1)菱形继承带来的主要问题是,子类继承两份相同的数据,导致资源浪费;
2)利用虚继承可以解决菱形继承问题;
3)虚继承的原理:虚基类只继承其一个虚基类指针 vbptr 指向 虚基类表:其中保存着虚基类变量的偏移量,都指向一个变量;
图1:二义性问题;图二:可以使用作用域符号解决(但是还有有两个值);图三:使用虚继承解决冗余(只有一个数据了);
- 多态的基本使用、基本概念
多态是C++面向对象3大特性之一
多态分为两类:
1) 静态多态:函数重载、运算符重载 属于静态多态,复用函数名(c++中多态一般指动态多态)
2) 动态多态:派生来和虚函数实现运行时多态;
区别:
1)静态多态的地址早绑定 - 编译阶段确定函数地址
2)动态多态的地址晚绑定 - 运行阶段确定函数地址
多态的条件:
1)有继承关系
2)子类重写父类中的虚函数
3)父类指针或引用指向子类对象
注:重写:函数返回值、函数名、参数列表完全一致
- 多态原理剖析:
1)基类中定义虚函数后,类内生成一个非静态成员变量(隐藏的):vfptr虚函数指针,占用类内空间(4/8字节)
2)vfptr指向当前类内的虚函数表,其中存放着类内的虚函数地址
3)当子类不重写父类虚函数时:子类中的vfptr指向子类的vftable,但虚函数表中存放的是父类虚函数地址;
4)当子类重写父类虚函数时,子类中的vfptr指向子类的vftable,但这个虚函数表中存放的是子类重写的函数地址
- 多态:虚析构与纯虚析构(子类的堆区内存释放)
多态使用时,如果子类中有属性在堆区开辟内存,那么父指针在释放时,无法调用到子类的析构函数代码,怎么解决?
将父类中的析构函数改为 虚析构函数或纯虚析构函数(注意!父类)
共性:
1)都可以解决使用父类指针释放子类对象时,不会漏掉子类析构函数的调用
2)都需要写出具体的函数实现(纯虚的需要在类外实现)
语法:
1)虚析构:virtual ~类名() {}
2)纯虚析构:virtual ~类名()=0;类外:类名::类名(){};
总结:
1)虚析构或者纯虚析构就是用来解决通过父类指针释放子类对象的问题
2)如果子类中没有堆区数据,可以不写虚析构
3)拥有纯虚析构函数的类,也属于抽象类;不能实例化
before
- 有数组定义int a1[2][2]={{1,2},{2}};则a1[1][1]的值为不确定的
错的:这是一种初始化方法,第二排数据都初始化为2; - 关于宏定义中的# 和##
#字符串化, 例如:
#define LogMessage(a) printf("The message is: %s", #a);
LogMessage(WARNING) //The message is:WARNING
string ppp="abcdef";
LogMessage(ppp) //The message is: ppp; not "abcdef"
##符号连接操作 例如:
#define Sum_Number(b) Num##b
#define Num9 999
Sum_Number(9) //999
#@ 字符化,例如:
#define TEST(tp,charactor) \
{ \
char a[100]{0};\
sprintf_s("%s,%c", #tp, #@charactor); \
TRACE(a); \
}
TEST(WARNING, C); //输出,WARNING, C
```cpp
#include <stdio.h>
#define XNAME (n) x ## n
#define PRINT_XN (n) printf("x" #n " = %d\n", x ## n);
“#n” 表示把n变成字符串 ##n表示将两个语言符号合成一个
int main(void)
{
int XNAME(1) = 14; //变为 int xi = 14;
int XNAME(2) = 20; //变为 int x2= 20;
PRINT_XN(1); //变为printf("x1 = %d\n", x1);
PRINT_XN(2); //变为printf("x2 = %d\n", x2);
return 0;
}
x1 = 14
x2 = 20
-
关于赋值运算符:a=(b=4)+(c=6) 是一个合法的赋值表达式
operator = 返回本对象的引用;所以c++中完全没问题;在c中同样ok,先执行括号中的赋值运算,再进行加法运算;a = 10; -
关于struct中数据地址对齐的问题,当前变量的地址偏移量必须是当前变量所占字节的整数倍;
加上#pragma pack(4)后:
变量存放的起始地址的偏移量有两种情况:第一、如果n大于等于该变量所占用的字节数,那么偏移量必须满足默认的对齐方式,第二、如果n小于该变量的类型所占用的字节数,那么偏移量为n的倍数,不用满足默认的对齐方式。结构的总大小也有个约束条件,分下面两种情况:如果n大于所有成员变量类型所占用的字节数,那么结构的总大小必须为占用空间最大的变量占用的空间数的倍数,否则必须为n的倍数;
详情看这篇文章: 关于地址对齐的详解
举例:#pragma pack(4)//设定为4字节对齐 struct test { char m1; double m4; int m3; };
有#pragma: 1 + 3 + 8 + 4 = 16
无: (1 + 7 + 8 + 4)== 20 不是8的整数倍:20+4 = 24
#pragma pack(16) : 1 + (7) + 8 + 4 =20 不是整数倍 20 + 4 = 24; -
二叉树的深度是从1开始,根节点为1
高度从1开始,最低叶子结点为1
层数从1开始,根节点为1; -
满二叉树与完全二叉树:
左边满二叉树:全都是满的 右边完全二叉树:最下层右侧可以有空缺;
-
排序算法的时间空间复杂度