C++基础学习

关于数组

二维数组在内存当中存储的时候和一维数组一样是一块连续的空间,按照行进行存储的,第一行末尾后面是第二行的开头。
在函数参数传递的时候注意区分形参传递和引用传递(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等等。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值