new
//为a赋值为10
int a = new int(10)
int * arr =new int [10]
delete
delete a
delete[ ] arr
当你使用 delete
关键字删除一个动态分配的对象时,会调用该对象的析构函数。
引用
本质——本质是个指针常量,int * const ref = &a;
基本语法
int a=10;
int & b =a
注意事项
· 必须初始化
初始化后不能修改
常量引用
避免在使用的过程中被修改
可以使类似10的常量被引用;int & ref = 10;
做函数参数
可以像地址传递一样交换值大小
做返回值
不要返回局部变量引用。函数结束后,栈区内存释放,局部变量消失,引用将成为类似野指针的存在
函数
默认参数 func(int a,int b = 10)
占位符 func(int a , int )
函数重载
取决于参数列表中的:参数类型,参数个数,参数顺序
访问权限
public:类内外都可以访问,可以通过对象和成员方法访问
protected:类内可以访问,可以被子类继承。可以通过成员方法访问
private:类内可以访问,父类单独享有。只能通过成员方法访问
注意: 在C++中,术语“内部”和“外部”通常用于描述类的成员函数对类成员的访问权限。
-
内部成员函数:内部成员函数是指属于同一个类的成员函数,在这些成员函数中,可以直接访问该类的所有成员,包括私有成员。因此,内部成员函数对类的所有成员都有访问权限。
-
外部成员函数:外部成员函数是指不属于该类的成员函数,在这些函数中,只能通过类的公有接口来访问类的成员,不能直接访问类的私有成员。外部成员函数只能访问公有成员和保护成员,不能访问私有成员。
这里的“内部”和“外部”主要指的是函数在类的内部定义(成员函数)还是在类的外部定义(非成员函数)。内部成员函数可以直接访问私有成员,而外部成员函数不能直接访问私有成员。
struct和class在c++的唯一区别:访问权限的不同。class默认私有,struct默认公开
构造函数 类名(){}
调用规则
一开始提供午餐构造,有参构造,拷贝构造
手写有参构造后,不在提供无参构造
手写拷贝构造后,不再提供其它构造
析构函数 ~类名(){}
拷贝构造函数的调用时机 类名(const 类名 &p)
注意:使用引用是为了避免无限循环。例如在以下第一种情境中,调用拷贝构造函数时,如何还是值传递,就会再一次调用拷贝构造函数,造成无限循环
1.用已经创建的对象 对对象 初始化
2.值传递为函数参数的时候
3.值传递返回局部变量的时候
本质上都是创建副本所需。
深拷贝,浅拷贝
主要针对的是堆区储存的内容
对于浅拷贝,如果已经释放一次的内容再次被释放,就会出错
深拷贝通过重写拷贝构造函数,解决了这一问题。
eg. height = new int(*p.height) 在堆区重新申请一篇内存储存数据,即使原本的数据被释放,深拷贝的数据仍在堆区存在。
初始化列表 构造函数():A(),B(),C(){}
如果一个类的普通数据(类似ABCD)有很多,一个一个赋值会很麻烦,初始化列表可以简化这个过程;
类对象作为类成员时,构造和析构的运行顺序
class A{
B b;
}
构造函数,先调用B的构造函数,在调用A的构造函数
为了保证构造A的时候,B已经完成初始化。
析构函数,先调用A的析构函数,再调用B的构造函数
为了保证析构A的时候,B还在,还可以对其进行操作
静态成员(变量/函数)
可以通过类名/对象进行数据的访问
静态变量:类内定义,类外初始化 int person::A =100
静态函数:只能访问静态成员变量。因为静态成员函数不属于任何特定的对象实例,而是属于类本身。因此,静态成员函数在执行时没有隐式的 this
指针指向对象实例,无法直接访问非静态成员变量,因为非静态成员变量是属于对象实例的。
只有非静态成员变量,才属于类的对象
this指针的用途
1.避免成员变量和形参名字冲突
2.链式编程,返回用return *this(用引用的方式返回)。
此处涉及的问题是,如果不用引用的方式返回,将会返回一个P的副本(此处称为P'),那么链式编程中,下一次调用函数的就不再是P,而是P'。这将会造成P的预期结果由执行三次的40——变为只执行一次的结果20.
const修饰的 常函数,常对象
mutable修饰的变量仍然可以修改
成员函数本身隐含有this指针(指针常量,方向已经不可改变),再加上const修饰即不可改变值和方向
常对象只能调用常函数。因为调用其它成员函数的时候,可以改变成员变量的值,和定义不符。
友元(关于你的基友想要进入你房间的这件事)
1.全局函数作友元
2.友元类——类作友元
3.成员函数作友元
刚开始我纠结的是:课程中用的是指针指向对象,书本用的是引用。但是实际上两者下过是一样的,代码大致上相同
运算符重载
一般有成员函数重载/全局函数两种方式
加号—— 返回值类型 operator+(),两种方式都可以
左移运算符——cout是ostream的一个对象,只能用引用传递
不能用成员函数的方法重载。因为这么做的话,p<<cout与预计的cout<<p顺序相反
只能用全局函数重载 返回值类型 operator<<(ostream cout,person p)。又因为<<相当于链式编程,前面说到过,(P,P',P"导致结果为20,而不是40那里)所以返回值必须为cout 即返回值类型为ostream
递增运算符
在左移运算符重载的基础上进行
前置递增
基本逻辑
void operator++(){
A++;
}
对于基本类型变量,可以连续自加
int a =0;
cout<<++(++a)<<endl;
//a=2
为了实现这样的效果,需要将自加后的对象返回
MYINT& operator++(){
A++;
return *this;
}
这里用引用的方式返回有两个理由:第一,为了防止值传递返回副本。虽然这次结果输出正确,但之后再调用A的值会显示1(只有一次前置自加是对A,第二次操作相当于A');第二,这次返回的是对象本身(*this),本身就需要自身的值
后置自增
首先,如何与前置自增区分?——占位符
基本逻辑
void operator++(){
MYINT temp = *this;//用临时变量保存原始值
A++;
return temp;
}
与连续前置自增不同,(A++)++是非法的。
这样的话,就不必纠结返回值类型(毕竟不连续用,这一次没问题就行)
完善逻辑
MYINT operator++(){ 此处用值的方式返回temp,因为temp是局部变量,完成后置自增后,temp就要从内存中去除的
MYINT temp = *this;
A++;
return temp;
}
赋值运算符重写
目的
1.解决浅拷贝的问题
2.实现赋值运算符的连等操作
基本逻辑
person& operator=(person &p){这里必须使用引用的方式
如果用副本做参数,那么副本的指针部分将进行浅拷贝
这样一来,跳出此函数之后,原本有值的堆区就被释放了
if(m_age!=null){
delete m_age;
m_age = null;
}
提供深拷贝
m_age=new int(*p.m_age)
实现连等
return *this;
与实现连续自加一样,以引用返回对象本身
}
什么时候用&,什么时候值传递
——我知道两种
——1.不想被修改的用值传递,允许被修改的用&
——2.局部变量,重载符号连用等特殊情况
关系运算符重载
这里举例子的是==和!=,简单的很,就不做赘述了
仿函数——重载()的成员函数
在学习STL之前,可能会认为仿函数和普通函数之间的区别不太明显,因为仿函数并不是初学者通常接触到的概念。在初学者阶段,可能更多地关注函数的基本使用和语法。
但是,一旦开始学习STL(标准模板库),特别是学习算法部分时,仿函数的重要性就会显现出来。STL的许多算法都可以通过传递仿函数来实现定制化的操作,比如排序算法中的比较、查找算法中的判断等。在这些场景下,仿函数的灵活性和可定制性就会得到充分的展现。
因此,尽管初学者可能觉得仿函数和普通函数之间的区别不大,但在学习STL并深入了解算法和数据结构时,了解仿函数的概念和用法会变得至关重要。
继承
基本语法——class student :继承方式 父类类名{ }
父类中的私有,子类都访问不到
——虽然访问不到,但是这部分仍然被子类继承了(只是被隐藏了)
继承方式有 public,protected,private三种。
每种方式都是继承过来的成员变量的最低权限;只有public可以在全局函数中(类外)访问,private和protected只能在类内访问;
构造和析构的顺序
与类对象做类成员不同:“构造——先内后外;析构——先外后内”。此处的顺序为:“构造——先父类后子类;析构——先子类后父类”
同名成员
——作用域立大功
同名静态成员
——作用域立大功。不同的是,有两种方式可以访问:“通过对象”,“通过类”
菱形继承
——虚继承:继承方式前+virtual()
虚继承的实现涉及了一些底层机制,其中一个重要的概念是虚基表(vtable)和虚基指针(vptr)。
-
虚基表(vtable):每个包含虚函数或虚基类的类都会有一个虚函数表(vtable),虚基类的信息也存储在这个表中。虚基表中存储了虚基类的偏移量,用于在派生类中定位虚基类的成员。
-
虚基指针(vptr):对于包含虚函数或虚基类的每个类的对象,都会有一个指向虚表的指针,称为虚指针(vptr)。虚指针位于对象的内存布局的开头,它指向对象所属类的虚表。
在使用虚继承时,派生类中会包含一个指向虚基类子对象的偏移量,这个偏移量存储在派生类对象中的虚表中。这个偏移量用于定位虚基类子对象在派生类对象中的位置。
当通过派生类对象访问虚基类成员时,编译器会使用虚指针和虚基表来定位虚基类成员的实际地址。这样,即使派生类中包含多个虚基类子对象,也可以正确地访问每个虚基类的成员。
总的来说,通过虚指针和虚表的机制,编译器能够在派生类对象中正确地定位和访问虚基类的成员,从而实现了虚继承的功能。
多态
基本语法
原理
虚函数和纯虚函数
——已经知道虚函数在父类中通常是无意义的。
纯虚函数:俺来啦!!!!
——值得注意的是,没有具体的函数实现(要么怎么叫纯虚呢hhh)
——这里第一次出现抽象类的概念(虽然学java的时候也接触过。我的理解就是,不能实例化对象,天生就是被用的)
虚析构和纯虚析构
——继承中我们接触过构造函数和析构函数的顺序。但是在多态会遇到一个新的问题:“析构函数不调用子类析构,只调用父类析构”,这会导致子类在堆上的内容清理不干净。
——需要注意的是虚析构和纯虚析构需要有函数实现部分。
文件(带我回校翻翻教材,不记得要不要学了)
泛型编程——模板(明确目的:不是为了写模板,而是为了运用STL)
概念
——提高代码的复用性。模板只是框架,不能直接使用。
函数模板
基本语法
——template<typename T>
——使用方式
注意事项
1.自动类型推导,必须推导出一致的数据类型T
2.模板必须指定T的数据类型才可以使用(???)
(有时可以依靠自动类型转换而忽略“指定T”这一操作),尽管这样,这一步依然存疑,感觉是多余的操作。
普通函数与函数模板的区别
优先显示指定类型,因为我们明明知道他要用什么,省的编译器到时候又大惊小怪
——注意事项中提到过,自动类型推导必须推出与‘T’数据类型一致才行。
之前的理解:“int先有,后来进入的char不被识别为T的数据类型”
正确的理解:“既有int,又有char,T并不知道谁才是对的,所以报错”
普通函数与函数模板的调用规则
空模板参数:“<>”
模板的局限性
这种问题与“比较两个类对象的大小”是同样的问题。之前通过运算符重载解决,这次通过具体化模板实现。
类模板
“模板必须指定T的数据类型才可以使用”在这里可以有新的理解:“之前显示不出必要性,这里则显示的淋漓尽致。实际上是为了符合规范“使用显示指定类型”。”
类模板与函数模板的区别
第一条,合理的解释了上一条“选择使用显式指定类型”的原因——类模板没有自动类型推导
第二条,类模板的模板参数列表中可以有默认参数。为什么模板函数中不可以呢?因为模板函数的T只有一个,只要有一个确定类型(比如int),那其他的也都定死了,相当于把一个模板函数写死成普通函数了。
类模板与函数模板成员函数的调用时机
截至到P177