关于数组
二维数组在内存当中存储的时候和一维数组一样是一块连续的空间,按照行进行存储的,第一行末尾后面是第二行的开头。
在函数参数传递的时候注意区分形参传递和引用传递(void test(int &a)),形参传递是将原参数复制一份传递,函数过程中不修改原数据的值,引用传递则是会改变原数据的值。当然也可以通过传递地址来进行实参的修改void test(int * a);此时调用的时候应该使用test(&a);
函数可以声明多次,但是只能定义一次,如果多次定义则会报错。
关于指针:
指针本身也是一个变量,这个变量保存的是一个地址。
指针占用的空间:在32位OS下占4B,64位OS下占8B(不管是哪种类型的指针都是如此,其实也很容易理解,因为指针里面存的是地址)
空指针和野指针:
空指针:指针指向内存中编号位0的空间的指针。内存条的每个地址是有编号的,从0开始一直往后,如果某个指针指向的位置是0,那么这个指针就是空指针,空指针指向的内存(编号为0的内存)是不可以访问的(里面存的是)。空指针的意义:初始化指针,如果在定义指针的时候不知道赋值为什么好,那就可以赋值为空指针(NULL)。
野指针:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)指针变量在定义时如果未初始化,其值是随机的,指针变量的值是别的变量的地址,意味着指针指向了一个地址是不确定的变量,此时去解引用就是去访问了一个不确定的地址,所以结果是不可知的。
NULL和nullptr的区别:
在C语言中NULL被宏定义为一个(void*)类型的指针( #define NULL (void*)0 ),在C++中NULL就是整数0( #define NULL 0 ),所以C++中更推荐使用nullptr作为空指针的初始化。
Const修饰指针:
const int * p = &a; 指针的指向可以改(指针可以指向其他位置),但是指向的内容(指向的值)不能改。
int * const p = &a; 指针的指向不能改(指针固定指向这一块内存),但是内存里面的数据可以改。
const int * const p; 指针的指向和值都不能改。
关于结构体:
声明结构体的时候struct关键字不能省略,使用结构体初始化变量的时候可以省略struct关键字。注意声明结构体的时候大括号后面还有个分号!!
结构体多关键字排序
struct student{
int a;
int b;
}stu[10];
bool cmp(student s1 , student s2)
{
if(s1.a == s2.a)
{
return s1.b > s2.b;
}
else
{
return s1.a > s2.a;
}
}
sort(stu , stu + 10 , cmp);
结构体指针
int main()
{
student ss = {0 , 1};
student * p = &ss;
cout << p->a << " " << p->b << endl;
return 0;
}
结构体嵌套很简单,可以使用teacher.student.score = 80;这样的方式进行赋值和输出。
结构体中const的应用
通过使用const关键字可以防止结构体内容被修改,如果修改的话会报错,主要用于结构体的地址传递的过程。
void printstu(const student * stu)
{
stu -> a = 100;//会报错
cout << stu -> a << " " << stu->b << endl;
}
面向对象
内存分区模型
C++程序在执行的时候会将内存分为4个大区域,分别是代码区、全局区、栈区和堆区。
代码区:存放写的所有代码的字符
全局区:静态变量和全局变量
栈区:由编译器自动分配和释放,存放函数的参数值、局部变量等
堆区:由程序员进行分配和释放,如果程序员不释放,则程序结束时操作系统会自行回收。
内存分区的意义:不同区的生命周期不同,能够给编程更大的灵活性
程序运行前
程序经过编译和链接之后会生成exe文件,在exe文件运行前,代码区和全局区已经存在了。
代码区存放的是CPU要执行的机器指令,具有共享性和只读性两个特点。共享性指的是如果该程序被反复执行,那么每次执行的都是同一份代码,而不是将相同的代码进行拷贝。只读性是为了防止其它程序修改该程序的指令。
全局区:存放全局变量、静态变量(static),除此之外还能存放常量,包括字符串常量、const修饰的全局常量。该区域的数据在程序结束后由操作系统释放。
注意:const修饰的局部变量(局部常量)不在全局区!!!!
程序运行后
栈区:存放函数的参数值、局部变量等。里面的数据由编译器负责开辟和释放。这里要注意不要去return局部变量的地址,因为栈区的数据在函数执行完之后会自动释放,拿到的地址是没有意义的(这块地址已经被释放过了)。
堆区:由程序员分配和释放,若程序员不释放,程序结束时由OS回收,C++中主要利用new关键字在堆区开辟内存。
int * func()
{
int *p = new int(10);//如果括号里面不填数字,则会默认赋值为0
return p;
}
int main(){
int *p = func();
cout << *p << endl;
cout << *p << endl;//只要程序员不手动释放,在程序运行结束前这个空间里的值一直存在,所以无论打印多少次都是10
return 0;
}
new/delete关键字
new关键字返回的是对应数据类型的指针,所以要用一个指针变量去接收,所创建的数据存放在堆区。
int main(){
int * p = new int(100); //在堆区开辟一个int,赋值为10,返回这个数据的地址
int * arr = new int[10]; //在堆区开辟一个长度为10的int型数组,返回首地址
for(int i = 0 ; i < 10 ; i ++)
{
arr[i]= i; //给数组赋值
}
delete[] arr; //释放数组,这里注意加上[]
delete p; //释放整数
return 0;
}
引用
基本语法
引用就相当于给变量起别名,语法: 数据类型 &别名 = 原名
int a = 10;
int &b = a;
这里注意a和b指的是同一块内存,修改a或者b都是改的这一块内存中的数据。
注意事项
1、引用必须要初始化: int &b;是错误的。int &b = a;才是正确的。
2、引用在初始化之后不能发生改变,也就是说成为某个变量的引用之后不能再成为其它变量的引用
3、一个变量可以有多个引用
int a = 10;
int c = 20;
int &b = a;//是正确的
b = c;//也是正确的,这里是赋值操作不是更改引用,此时a,b,c都是20
4、引用的权限不能放大
const int a = 10;
int &b = a;//这是错误的,因为原本a只读,这里b增加了修改的权限
5、引用必须是一个变量,不能引用常量(引用必须是一块合法的内存空间)
原则就是:能改的可以不改,但是不能改的一定不能改!
int a = 10;
int &ref2 = a;//合法,常规操作,a和ref都能改
/* 把一个能改的赋值给不能改的,合法 */
const int &ref1 = a; //合法,a能改,但是ref不能改,a改了ref也会改
/*
把一个不能改的赋值给能改的,不合法
*/
int &ref3 = 10;//不合法,不能改的赋值给能改的
/*
合法,不能改的赋值给不能改的
更进一步,编译器会对这个代码进行优化:
int temp = 10;
const int &ref = temp;
也就是说还是使用变量去进行赋值的
*/
const int & ref4 = 10;
const int b = 10;
const int & ref5 = b;//合法,不能改的赋值给不能改的
引用和指针的区别
引用(本质是一个指针常量)和指针一样都可以指向内存的某一个空间,不同的是引用只是别名,看似“不占用内存空间”,指针是个变量,本身要占用一段内存空间。实际上引用和指针一样也是要占用一块内存空间的(不然怎么知道引用的地址在哪里),但是编译器会对引用进行优化,使得其看起来只是一个单纯的别名,但是实际上是占用了和指针一样大小的一块的空间的,只不过这块空间无法去读取到具体在哪里。
引用作为函数的返回值
1、不要返回局部变量的引用
int& func()
{
int a = 10;
return a;
}
int main(){
int& ans = func();
cout << ans << endl; //报段错误
return 0;
}
报段错误的原因在于局部变量是存放在栈区里面的,当func函数运行完毕后空间就被释放了,所以此时对它的引用也就无效了。
2、函数调用可以作为左值(放在等号左边)
int& func()
{
static int a = 10; //注意是static
return a;
}
int main(){
int& ans = func();
cout << ans << endl;
func() = 20; //函数调用放在左边
cout << ans << endl;
return 0;
}
结果如下
10
20
因为func返回的是引用,可以看作把a的别名返回了,也就是把a返回了。这上面的赋值语句等效为 a = 20;
引用的本质
引用的本质其实就是一个指针常量int * const ref,例如
void func(int &a)
{
//编译器也会转换成*a = 100
a = 100;
cout << a << endl;
}
int main(){
int a = 10;
/* 编译器帮我们转换成为int * const ref = &a ,
指针常量的指向不能改,所以引用也不能再去引用其它地方。
指针常量引用的值可以改,所以引用的值可以改 */
int & ref = a;
ref = 20;//编译器转换为*ref = 20
cout << "a = " << a << endl;
cout << "ref = " << ref << endl;
return 0;
}
常量引用
可以在函数的形参列表中使用,用来防止形参被误操作修改
void func(const int &a)
{
a = 100; //会报错
cout << a << endl;
}
int main(){
int a = 10;
int & ref = a;
cout << "a = " << a << endl;
cout << "ref = " << ref << endl;
return 0;
}
函数提高
默认参数
int func(int a , int b = 10 , int c = 20)
{
return a + b + c;
}
int main(){
cout << func(30) << endl; //60
cout << func(10,100,100) << endl; //210
return 0;
}
如果传入了参数那么使用用户传入的参数,如果没有的话使用默认值。
这里要注意:
1、如果b的位置有默认参数了,那么它后面的cdefg……都要有默认参数。
2、如果函数声明和函数实现不能同时有默认参数(声明和实现分开的情况下),否则会报重定义的错误。
int func(int a , int b = 10 , int c = 20);
int main(){
cout << func(30) << endl; //60
cout << func(10,100,100) << endl; //210
return 0;
}
int func(int a , int b = 10 , int c = 20)//会报错
{
return a + b + c;
}
函数重载
多个函数的名称相同、参数不同
重载的满足条件:同一个作用域下,函数名称相同,函数参数的类型、个数、顺序不同。
函数的返回值不是重载的条件!
引用作为重载条件
void func(int &a){
cout << "func1" << endl;
}
void func(const int &a){
cout << "func2" << endl;
}
int main(){
int a = 10;
func(a); //func1,参数是个变量
func(10); // func2,参数是个常量
return 0;
}
重载碰到默认参数
void func(int a){
cout << "func1" << endl;
}
void func(int a , int b = 10){
cout << "func2" << endl;
}
int main(){
int a = 10;
func(a); //报错,两个函数都能调用,会产生二义性
return 0;
}
封装、继承和多态
C++的类
const double PI = 3.1415926;
class Circle{
public: //访问权限修饰符
double r;
double returnC()
{
return 2 * PI * r;
}
}; //别忘了这里的分号
int main(){
Circle c1;
c1.r = 100;
cout << c1.returnC() << endl;
return 0;
}
包含get/set方法的类
const double PI = 3.1415926;
class Circle{
private:
double r;
public:
double returnC()
{
return 2 * PI * r;
}
void setR(double r){
this->r = r;
}
double getR(){
return this->r;
}
};
int main(){
Circle c1;
c1.setR(30);
cout << c1.getR() << endl;
cout << c1.returnC() << endl;
return 0;
}
访问权限
一共有三种访问权限:public、protected、private
public:类内类外都能访问
protected:类内可以访问,类外不行,子类可以访问父类中的保护内容
private:类内可以访问,类外不行,子类不能访问父类中的私有内容
struct和class的区别
区别是默认的访问权限不同,strcut默认是公共(public),class默认的是private。
构造函数和析构函数
构造函数用于初始化,析构函数用于清理,和Java类似,构造函数和析构函数整个程序中都只会被调用一次。构造函数和析构函数没有返回值,名称和类名保持一致,放在public权限里面。构造函数可以有参数,能重载,析构函数没有参数因此也不能重载。
#include<bits/stdc++.h>
using namespace std;
const double PI = 3.1415926;
class Person{
private:
string name;
int age;
public:
Person(){
cout << "构造函数" << endl;
}
~Person(){//注意写法
cout << "析构函数" << endl;
}
string getName(){
return this->name;
}
void setName(string name){
this->name = name;
}
int getAge(){
return this-> age;
}
void setAge(int age){
this->age = age;
}
};
int main(){
Person p;
p.setAge(18);
p.setName("zhangsan");
cout << p.getAge() << " ==== " << p.getName() << endl;
return 0;
}
结果如下
构造函数
18 ==== zhangsan
析构函数
注意析构函数只会在对象被销毁前进行调用,而且只调用一次,如果在return 0前面添加system(“pause”); , 那么则会出现下面的情况:
构造函数
18 ==== zhangsan
Press any key to continue . . .
析构函数
构造函数的种类
构造函数分为无参构造(默认)和有参构造,有参构造又分为普通有参构造和拷贝构造,如下所示
Person(){
cout << "无参构造函数" << endl;
}
Person(int age){
setAge(age);
cout << "有参构造函数" << endl;
}
Person(Person &p){
setName(p.getName);
setAge(p.getAge);
cout << "拷贝构造函数" << endl;
}
其中拷贝构造函数是复制一个完全一样的对象,需要传入的也是一个对象而不仅是参数,如下所示
int main(){
Person p1;
p1.setAge(18);
p1.setName("zhangsan");
cout << p1.getName() << " ==== " << p1.getAge() << endl;
Person p2(p1);
cout << p2.getName() << " ==== " << p2.getAge() << endl;
return 0;
}
结果如下:
无参构造函数
zhangsan ==== 18
拷贝构造函数
zhangsan ==== 18
析构函数
析构函数
需要注意的是拷贝构造函数常常使用如下形式。
Person(const Person &p){
setName(p.getName());
setAge(p.getAge());
cout << "拷贝构造函数" << endl;
}
但是这种带const的形式不能很好的和this关键字兼容。其中一个解决方法是把所有的get/set方法也变成const的方式。
调用默认构造函数的时候直接实例化对象即可,不需要后面加括号,因为加上括号会让编译器以为是一个函数的声明
Person p1;//正确的
Person p1();//错误的
匿名对象
Person(10);
没有左值去接收,运行结束之后立刻被OS回收。也不能利用拷贝构造函数初始化匿名对象,例如
Person p2 = Person(p1); //合理
Person(p2); //不合理
这样编译器会认为Person(p2) ===== Person p2 , 两者没什么区别。
隐式转换
构造函数还有一种调用方式,利用了匿名对象
Person p4 = 10;//相当于Person p4 = Person(10);
拷贝构造函数的调用时机
1、使用一个已经创建完毕的对象来初始化一个新对象,这个很常用,不说了
2、值传递的方式给参数传值
void doWork(Person p)
{
cout << "haha" << endl;
}
void test(){
Person p;
doWork(p);//此时会调用拷贝函数
}
3、以值方式返回局部对象
Person doWork()
{
Person p1;
cout << "haha" << endl;
return p1;
}
void test(){
//此时会调用拷贝函数,拷贝一个新的对象返回,p和函数里面的p1的地址是不一样的
Person p = doWork();
}
构造函数调用规则
C++默认给每一个类添加无参构造、析构函数和拷贝构造三个函数。
如果用户定义了有参构造,C++不提供无参构造,但是会提供默认拷贝构造
如果用户自定义拷贝构造函数,C++不提供其他构造函数。
深拷贝和浅拷贝
浅拷贝:简单的复制拷贝操作(编译器提供的)
深拷贝:在堆区重新申请空间进行拷贝操作
浅拷贝会遇到的问题是:
如果类里面存在指针变量,当调用析构函数的时候会释放掉指针所指向的空间,此时如果被拷贝的对象也要调用析构函数话,这段空间就会被重复释放从而引发错误。
深拷贝会在堆区中重新开辟一块空间用来拷贝指针指向的位置,不会出现重复释放的情况。
自己实现深拷贝函数:
class Person{
public:
string name;
int age;
int * height;
public:
Person (string name , int age){
this->name = name;
this->age = age;
cout << "有参构造" << endl;
}
Person(const Person &p){
age = p.age;
name = p.name;
//height = p.height; //编译器默认实现的浅拷贝
height = new int(*p.height); //深拷贝
cout << "拷贝构造函数" << endl;
}
~Person(){
cout << "析构函数" << endl;
}
};
int main(){
Person p1("haha" , 18);
int a = 170;
p1.height = &a;
cout << p1.name << " ==== " << p1.age << " ==== " << p1.height << endl;
Person p2(p1);
cout << p2.name << " ==== " << p2.age << " ==== " << p2.height << endl;
return 0;
}
结果如下:
有参构造
haha ==== 18 ==== 0x62fdac
拷贝构造函数
haha ==== 18 ==== 0xeb17e0
析构函数
析构函数
可以看到现在的两个height是不同的地址,因此不会出现重复释放的情况
类的嵌套
如果A类里面有一个B类作为成员(A包含B),构造的时候先构造B类再构造A类,析构的时候先析构A类再析构B类。
静态成员(static)
不管是静态成员变量还是函数(方法),内存中都只有一份,被所有的成员所共享。
静态成员变量:
1、所有对象共享同一份数据(属于所有对象,一个对象改了,所有对象都受影响)
2、编译阶段分配内存
3、类内声明,类外初始化
4、有访问权限
#include<bits/stdc++.h>
using namespace std;
class Person{
public:
static int a; //类内声明
private:
static int b; //静态成员变量也是有访问权限的
};
int Person::a = 100; //类外初始化
int Person::b = 200;
int main(){
Person p1;
cout << p1.a << endl; //通过对象访问
cout << Person::a << endl; //通过类直接访问
cout << p1.b << endl;//会报错,b是私有的,不能在类外面访问
return 0;
}
静态成员函数:
1、所有对象共享一个函数
2、静态成员函数只能访问静态成员变量
3、有访问权限
#include<bits/stdc++.h>
using namespace std;
class Person{
public:
static int a; //类内声明
int aa = 10;
static void func(){
cout << "静态成员函数" << endl;
cout << "静态成员函数 a = " << a << endl;
cout << aa << endl; //会报错invalid use of member 'Person::aa' in static member function
}
};
int Person::a = 100; //类外初始化
int main(){
Person p1;
p1.func();//通过对象访问
Person::func();//通过类直接访问
return 0;
}
this指针
this指针是隐含在每一个非静态成员函数(包括get/set方法)内的一种指针。this指针是本质是一个指针常量,也就是Person * const this(指向不能够修改)。主要有两种用法:
1、和java类似,用于区分成员变量和局部变量
void setAge(int age){
this->age = age;
}
2、用*this作为返回值指向当前对象
#include<bits/stdc++.h>
using namespace std;
class Person{
public:
int age;
//注意要用引用去接收,否则每次返回的都是一个新对象,而不是原始的对象
Person& func(){
this->age ++;
return *this;
}
};
int main(){
Person p1;
//p1的func方法返回的还是p1这个对象,可以继续调用func方法
p1.func().func().func().func();
cout << p1.age << endl; //结果是4
return 0;
}
const修饰成员变量
常函数:
使用const修饰的函数,常函数不能修改成员属性,除非成员属性在声明的时候加上mutable关键字
常对象:
使用const修饰的对象,常对象只能调用常函数
#include<bits/stdc++.h>
using namespace std;
class Person{
public:
mutable string name;
int age;
void func() const { //注意const的位置
this->age = 10;//会出错
this->name = "zhangsan";//没问题
}
};
int main(){
Person p1;
p1.func();
cout << p1.age << endl;
return 0;
}
在成员函数后面加上const的本质其实是修改this指针,this指针本身就是指向的位置不能够修改的,在前面再加上const就会变成 const Person * const this;这个时候this指针指向的位置和指向的值都不能修改了,所以常函数也没有办法修改成员属性。
这里注意和const int func(){}函数区分一下,const int func(){}这个函数的作用是不能修改返回值,实际上没什么用,加不加const没什么区别,因为返回值是一个固定的值
友元
程序中有一些私有的属性,但是也想让类外的一些特殊函数或者类进行访问,关键字是friend。
全局函数作为友元
#include<bits/stdc++.h>
using namespace std;
class Person{
friend void friendfun(Person * p); //全局函数作为友元,写在类的最上面就行
public:
string name;
Person(){
this->age = 18;
this ->name = "zhangsan";
}
private:
int age;
};
void friendfun(Person * p){
cout << "Person的name是:" << p->name << " , 年龄是:" << p->age << endl;
}
int main(){
Person p1;
friendfun(&p1);W
此时可以看到年龄被打印,说明已经成功访问了私有属性。
类作为友元
#include<bits/stdc++.h>
using namespace std;
class Person{
friend class Good;//类作为友元
public:
string name;
Person(){
this->age = 18;
this ->name = "zhangsan";
}
private:
int age;
};
class Good{
public:
Person * person;//通过指针指向要访问的类
Good(){
person = new Person;
}
void printinfo();
};
void Good::printinfo(){ //类外实现函数,加上作用域就可以了,但是注意类内要提前声明
cout << "Person的name是:" << person->name << " , 年龄是:" << person->age << endl;
}
int main(){
Good goodman;
goodman.printinfo();
return 0;
}
成员函数作为友元
#include<bits/stdc++.h>
using namespace std;
class Person;//声明一下Person类,这里必须要这样做,不能直接写Person类
class Good{
public:
Person * person;//通过指针指向要访问的类
Good();
void printinfo();
};
class Person{
friend void Good::printinfo();//成员函数作为友元
public:
string name;
Person(){
this->age = 18;
this ->name = "zhangsan1";
}
private:
int age;
};
Good::Good(){
person = new Person;
}
void Good::printinfo(){ //类外实现函数,加上作用域就可以了,但是注意类内要提前声明
cout << "Person的name是:" << person->name << " , 年龄是:" << person->age << endl;
}
int main(){
Good goodman;
goodman.printinfo();
return 0;
}
这个顺序好像不能变,先是声明被访问的类,然后定义友元类,在类外面实现相关方法(类中所有的方法都要实现)
运算符重载
运算符只能运算基本的数据类型,通过重载运算符可以运算其他的类型,比如说类于类。
重载加号运算符
#include<bits/stdc++.h>
using namespace std;
class Person{
public :
int a;
int b;
/* Person operator+ (Person &p){ //成员函数重载
Person temp;
temp.a = this->a + p.a;
temp.b = this->b + p.b;
return temp;
} */
};
Person operator+ (Person &p1 , Person &p2){ //全局函数重载
Person temp;
temp.a = p1.a + p2.a;
temp.b = p1.b + p2.b;
return temp;
}
int main(){
Person p1;
p1.a = 10;
p1.b = 10;
Person p2;
p2.a = 20;
p2.b = 20;
Person p3 = p1 + p2;
cout << p3.a << " === " << p3.b << endl;
return 0;
}
内置的数据类型表达式运算符不能进行重载
继承
语法: class 子类 : 继承方式 父类
继承方式
继承方式有三种,公有继承,保护继承,私有继承
公有继承:父类中的public和protected继承过来还是public和protected
保护继承:父类中的public和protected继承过来都是protected
私有继承:父类中的public和protected继承过来都是private
父类中是私有的类型,子类无法访问
继承中的对象模型
父类中所有的非静态成员都会被子类继承下去,私有的成员虽然访问不到,但是还是占有空间。如果父类中有三个int类型,子类中有一个自身的int,那么使用sizeof输出这个类的时候,结果是16。
继承中的构造和析构顺序
父类构造 -> 子类构造 -> 子类析构 -> 父类析构
继承同名成员处理方式
子类和父类出现同名成员的话,通过子类成员如何访问?
访问子类成员,直接使用.访问即可
访问父类成员要使用作用域
#include<bits/stdc++.h>
using namespace std;
class Father{
public :
int a = 100;
int b = 100;
void func(){
cout << "父类无参成员函数" << endl;
}
void func(int a){
cout << "父类有参成员函数" << endl;
}
};
class Son : public Father{
public:
int a;
int b;
void func(){
cout << "子类无参成员函数" << endl;
}
};
int main(){
Son s;
s.a = 10;
s.b = 10;
cout << s.a << " == " << s.b << " == " << s.Father::a << " == " << s.Father::b << endl;
s.func();
s.func(100); //会报错,其他的三个没问题
s.Father::func();
s.Father::func(100);
return 0;
}
如果子类中出现和父类同名的成员函数,子类的成员会隐藏掉父类所有的同名成员函数,也就是说父类中的函数重载会失效。只能通过添加作用域的方式去访问。
同名静态成员处理
和非静态是一样的
多继承
C++允许一个类继承多个类
多继承可能会引发父类中有同名成员的出现,需要加作用域去区分,实际开发中不太建议使用多继承
语法: class 子类 : 继承方式 父类1 , 继承方式 父类2……
菱形继承和虚基类表
B和C继承了A,D同时继承了B和C。如果D要访问A类中的属性,那么就会报错,因为B和C中都继承了这个属性,无法确定访问哪一个属性。除了二义性之外,还需要额外开辟空间去保存这些数据,也会造成空间浪费。因此有两种解决方式:加作用域和虚继承。
加作用域和之前一样,不多解释了。
虚继承是在继承之前加上关键字virtual,例如
class B : virtual public A{};
虚基类是通过B和C类的两个指针(vbptr)指向虚基类表,这个虚基类表中记录的是一个偏移量,类的基地址加上表中的偏移量就能够得到这个唯一的数据。这个数据只有一个,不管是从B还是C继承的,一共就一份。此时无论通过BC作用域访问还是D直接访问都访问的是同一个东西。
多态
多态分为静态多态和动态多态
静态多态:函数重载和运算符重载
动态多态:派生类(子类)和虚函数实现运行时多态
二者的区别:
静态多态的函数地址在编译阶段就确定了
动态多态的函数地址在运行阶段才确定
动态多态满足的条件:
1、有继承关系
2、子类重写父类的虚函数
动态多态使用条件:
父类的指针或者引用指向子类对象
#include<bits/stdc++.h>
using namespace std;
class Father{
public :
//void func(){ //此时输出我是父亲
virtual void func(){ //此时输出我是儿子
cout << "我是父亲" << endl;
}
};
class Son : public Father{
public:
void func(){
cout << "我是儿子" << endl;
}
};
void dofunc(Father & f){
f.func();
}
int main(){
Son s;
//父类接口传入子类对象
dofunc(s);//不加virtual关键字输出的是父亲,加了是儿子
return 0;
}
底层原理
使用virtual关键字的时候,类里面会多一个vfptr指针,指针指向虚函数表,这个表里面记录的是虚函数的地址。如果一个类继承了该类,那么也会对这个虚函数表进行继承(在子类里面再拷贝一份)。当子类重写父类函数的时候,父类指针或者引用再指向子类对象会发生多态。
子类重写之后,相同方法的对象就会变成子类,如果没有重写,则还是继承父类的方法。
https://www.bilibili.com/video/BV1et411b73Z?p=136&spm_id_from=pageDriver&vd_source=7798e79075a259e6d983b8f3a14d87d8
12:58
纯虚函数和抽象类
父类中虚函数的实现通常是无意义的,因为基本上都会被子类给重写,所以父类中的虚函数可以改为纯虚函数,语法如下:
virtual 返回值类型 函数名 (参数列表) = 0;
当类中有了纯虚函数,这个类被称为抽象类
抽象类的特点:
1、无法实例化对象
2、子类必须重写抽象类中的纯虚函数,否则子类也属于抽象类(子类也无法实例化对象)
实际上,写纯虚函数的意义就是在于想强制让子类重写父类中的函数。
虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到了堆区,那么父类指针在释放的时候无法调用到子类的西沟代码。
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构的共性:
1、可以解决父类指针无法释放子类对象的问题
2、都需要有具体的函数实现
虚析构和纯虚析构的区别:纯虚析构的类属于抽象类,无法实例化对象
语法
virtual ~类名(){}//虚析构
virtual ~类名() = 0;
STL
string
string的本质
string的本质是一个类,封装了一个char*。其实本质还是一个字符数组,char*返回的是这个字符数组的首地址。
string的拼接
string s1 = "abc" , s2 = "123";
s1 += s2;
s1.append(s2);
/* 除此之外也可以直接追加单个的字符 */
查找和替换
主要是三个函数,find(),rfind(),replace()
int main(){
string s = "abcdefg";
s.find("abc");//从左到右找,找到了返回第一次出现的下标,没找到返回-1
s.rfind("abc");//从右到左找,找到了返回第一次出现的下标,没找到返回-1
s.replace(1 , 2 , "66666");//从下标1开始的长度为2的字符串替换为后面的字符串
return 0;
}
比较
两个字符串按照ascii码进行比较,相等返回0,大于返回1,小于返回-1.
int main(){
string s = "abcdefg";
string ss = "abcdeff";
int ans = s.compare(ss);
if(ans == 0){ // ==
}else if(ans > 0){ // a > b
}else{ // b < a
}
return 0;
}
单个字符的存取
int main(){
string s = "abcdefg";
for(int i = 0 ;i < s.length() ; i ++){ //单个输出字符的两种方式
cout << s[i] ;
cout << s.at(i);
}
return 0;
}
也可以使用这种方法修改单个字符。
插入和删除
int main(){
string s = "abcdefg";
s.insert(2 , "666");//从下标2开始插入这个字符串
s.erase(2 , 2); //从下标2开始删除2个字符
cout << s << endl;
return 0;
}
子串
int main(){
string s = "abcdefg";
string sub = s.substr(2 , 2);
cout << sub << endl; //cd
return 0;
}
使用的场景是在固定格式的字符串中提取有用信息,比如从身份证号中提取生日,从邮箱地址中提取用户名。
int main(){
string s = "helloworld@sina.com";
int pos = s.find("@");
string ans = s.substr(0 , pos);
cout << ans << endl; //提取@前面的内容
return 0;
}
vector
vector和数组的区别:数组是静态的空间,vector可以动态扩展。动态扩展的意思是新开辟一块更大的空间,把原有的内容拷贝进去,然后释放原有空间。
vector可以使用尾插和尾删
一些常用方法
int main(){
vector<int> v;
vector<int>::iterator it1 , it2;
for(int i = 0 ; i < 10 ; i ++){
v.push_back(i);//尾插
}
for(int i = 0 ; i < 3 ; i ++){
v.pop_back();//删除后面3个数字
}
v.resize(2);//重新指定大小,若指定的空间比原来的小,则截取后面多余的部分
v.resize(50 , 20); //重新指定大小,若指定的空间比原来的大,多余的空间用20填充,默认用0填充
it1 = v.begin();
for(int i = 0 ; i < 3 ; i ++ , it1 ++);
v.insert(it1 , 66);//在下标为3的位置插入一个66
v.insert(v.begin() , 4 , 88);//在下标为0的位置插入4个88
v.erase(v.begin());//删除第一个元素
it1 = v.begin() , it2 = v.end();
for(int i = 0 ; i < 3 ; i ++){
it1 ++;
it2 --;
}
v.erase(it1 , it2);//把it1和it2之间的内容全部删除
cout << "v的容量" << v.capacity() << endl;
for(int i = 0; i < v.size() ; i ++){
cout << v[i] << " ";
}
v.clear();//清空
return 0;
}
这里要注意的是erase不会改变vector的大小,但是insert有可能会
杂七杂八
函数的参数传递方式
函数有三种传递方式,一种是值传递,形参不会修饰实参;一种是地址传递,形参会修饰实参;最后一种是引用传递,形参会修饰实参
#include<bits/stdc++.h>
using namespace std;
void swap1(int a , int b)
{
int tmp = a ;
a = b ;
b = tmp;
}
void swap2(int *a , int *b)//注意要取值
{
int tmp = *a ;
*a = *b ;
*b = tmp;
}
void swap3(int &a , int &b)
//别名和原名可以一样
{
int tmp = a ;
a = b ;
b = tmp;
}
int main(){
int a1 = 10 , b1 = 20;
int a2 = 10 , b2 = 20;
int a3 = 10 , b3 = 20;
swap1(a1 , b1);
swap2(&a2 , &b2);
swap3(a3 , b3);
cout << "after swap : a1 = " << a1 << " , b1 = " << b1 << endl;
cout << "after swap : a2 = " << a2 << " , b2 = " << b2 << endl;
cout << "after swap : a3 = " << a3 << " , b3 = " << b3 << endl;
return 0;
}
结果如下:
after swap : a1 = 10 , b1 = 20
after swap : a2 = 20 , b2 = 10
after swap : a3 = 20 , b3 = 10
空对象
每个空对象占用的空间是1B,原因是C++编译器会给每个空对象分配空间来区分空对象占内存的位置,也就是说每个空对象有一个独一无二的内存地址。
只有非静态成员变量存储在成员对象中
面经真题
string 和 char*的区别
string的本质是一个类,内部封装了char*。string类的内部还封装了很多成员方法,比如find,copy,delete,replace和insert等等。