C++基础速通笔记-上(持续补充)

内容太多了,打算分成上下两部

2024/10/31开写

一、速通

1.头文件与命名空间

1.1头文件

#include<iostream>

主函数只支持int main

//输出Hello World
#include<iostream>
using namespace std;//命名空间
int main(){
    cout<<"Hello World!";
    return 0;
}

1.2命名空间

//定义
namespace 空间{

}

//例子
namespace 王五{
    char 老婆;
}
namespace 张三{
    char 老婆;
}

作用:

·提高标识符的使用率

1.3命名空间的使用

#include<iostream>
using namespace std;//在最上面写上这个,下面就不用一直重复写了
namespace 大佬{
    char name[20] = "大佬";
}
int main(){
    printf("%s\n", 大佬::name);
    using namespace 大佬;//
    printf("%s\n", name);
    return 0;
}

2.标准输入与标准输出

2.1标准输入

//第一种
std::cout<<"输出"<<"\n";

//第二种
using namespace std;//这就是为什么写C++程序喜欢在最上面写这句话
cout<<"输出"<<endl;

示例:

#include<iostream>
using namespace std;
int main(){
    int x;
    char y;
    char z[10];
    cin>>x>>y>>z;
    cout<<x<<enl;
    cout<<y<<enl;
    cout<<z<<enl;

    return 0;
}

调试: 

//输入
10
a
abc
//输出
10
a
abc

3.内联函数、函数重载、缺省

3.1函数之内联函数

空间换时间:牺牲内存,加快运行速度

定义:

inline int Max(int a, int b){
    return a > b ? a : b;
}

//使用
int a = Max(1, 2)

利处:避免指令跳转,加快程序速度

弊处:代码多次被复制,增加代码量,占用内存

什么时候使用:内容少,代码短,功能简单

注:递归函数不能使用内联函数

3.2函数之函数重载

同样的函数名,不一样的参数

在同一个作用域内,可以声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同

void print(int a){
    cout<<a<<endl;
}

void print(int a, int b){
    cout<<a + b<<endl;
}

void print(float a){
    cout<<a<<endl;
}

void print(char a, int b){
    cout<<a + b<<endl;
}

void print(int a, char b){
    cout<<a + b<<endl;
}

注:二义性问题(print('A', 'B');),除非设置两个char

3.3函数之缺省

在函数的定义的时候对函数的参数进行初始化值;在调用的时候在缺省的参数的位置上可以不进行传参

C语言不支持设置缺省参数,C++则可以

注:只能从右往左依次缺省

定义:

void printData(int a, int b = 1001, int c = 1005, int d = 1009){
    cout<<a<<endl;
    cout<<b<<endl;
    cout<<c<<endl;
    cout<<d<<endl;
}

详细文章:C++ -缺省参数-详解_c++缺省参数-优快云博客

4.指针

4.1指针定义

//申请
#include<iostream>
using namespace std;
int main(){
    //1
    int* a = new int;
    *a = 1;
    //2
    int* b = new int(1);
    //3数组
    int* c = new int[2];
    *(c + 0) = 1;
    c[1] = 2;
    //4
    int* d = new int[2]{1, 2};
    
    //输出
    cout<<*a<<endl<<*b<<endl;
    cout<<c[0]<<c[1]<<endl;
    cout<<*(d + 0)<<*(d + 1)<<endl;
}

注:最后还要释放

delete a;
delete b;
delete c;
delete d;

4.2指针之内存再分配

//1.定义*memory
char *memory = new char[1024]{'\0'};

//开始分配(memoy + 0)的位置上,继续往后申请四个
int *mem = new(memory + 0) int[4]{1, 2, 3, 4}

//在memory的第四个int之后申请五个内存
Char *str = new(memory + sizeof(int)*4) char[5]{'A', 'B', 'C', 'D', '\0'};

//输出
cout<<str<<endl;
cout<<memory<<endl;
delete memory;

5.string类

#include<iostream>
#include<string>
using namespace std;
//string
int main(){
    string str1 = "Ilove";
    string str2(str1);
    string str3("Ilove");
    string str4;
    str4 = str1;
    cout<<str1<<endl;
    cout<<str2<<endl;
    cout<<str3<<endl;
    cout<<str4<<endl;
    
    cout<<str1.size()<<endl;
    strign str = "1";
    cout<<str1.find(str);
}

二、面向对象

2.1权限限定词(访问控制关键字)

·public 公有,可在类的外部被访问

·protected: 和private: 保护和私有(在继承有明显区别),不可被外界调用

·如果默认,那么就是私有的

·限定词的面对的的是类的对象,即:限定的是类的对象对于类中的属性和成员访问权限

·类中:任何成员之间可以互相使用

·类外:只能访问public成员

*补充:成员函数定义

1.在类内部定义成员函数

  • 方式:直接在类的内部编写函数体
class MyClass {
public:
    void myFunction() {
        // 函数体内容,例如:
        std::cout << "function defined inside the class." << std::endl;
    }
};
  • 特点
    • 这种方式定义的成员函数会被编译器当作内联函数(如果函数体比较简单)来处理,可能会提高程序的运行效率。不过编译器是否真正将其当作内联函数处理,还取决于函数的复杂程度等因素。
    • 函数可以直接访问类的所有成员(包括私有成员),因为它本身就在类的作用域内。

2.在类外部定义成员函数

  • 方式
    • 首先在类中声明函数,然后在类的外部(通常在对应的.cpp文件中)定义函数。声明时只需要写出函数的返回类型、函数名和参数列表,定义时需要使用作用域解析运算符::来指定函数所属的类。
class MyClass {
public:
    void myFunction();
};
void MyClass::myFunction() {
    std::cout << "function defined outside the class." << std::endl;
}
  • 特点
    • 这种方式使得类的声明和实现分离,有助于提高代码的可读性和可维护性。可以将类的接口(声明部分)放在头文件(.h.hpp)中,类的实现部分放在源文件(.cpp)中。
    • 对于比较复杂的函数,这种方式更容易组织代码结构,避免头文件过于臃肿。同时,也方便多个源文件共享类的实现代码,提高代码的复用性。

2.3构造函数

定义:

  • 构造函数是类中的一种特殊成员函数,它的名字与类名相同,没有返回类型(包括void)。它主要用于在创建对象时初始化对象的成员变量,是对象初始化的关键部分。

 作用:

  • 保证对象初始化的规范性:在类中定义构造函数可以确保每个对象在创建时都按照一定的规则进行初始化。如果没有构造函数,对象的成员变量初始值可能是不确定的,这会导致程序行为难以预测。
  • 封装和隐藏初始化细节:类是一种封装的机制,构造函数作为类的一部分,将对象初始化的细节隐藏在类内部。外部代码在创建对象时,只需要调用构造函数并提供必要的参数,而不需要了解对象内部是如何进行初始化的,提高了代码的安全性和可维护性。
  • 与类的整体关联性:类是对象的模板,构造函数紧密地与类结合在一起,体现了对象从无到有的创建过程。它是类的自然组成部分,使得对象的初始化过程与类的定义相统一,符合面向对象编程的理念。

 用处:

  • 初始化成员变量:最主要的作用是为对象的成员变量赋初始值。这可以是简单地设置默认值,也可以根据传入的参数进行有针对性的初始化。例如,一个Person类的构造函数可以根据传入的姓名、年龄等参数初始化对应的成员变量。
  • 执行初始化相关的操作:除了初始化成员变量,还可以在构造函数中执行其他与初始化有关的操作,如打开文件、分配内存、建立数据库连接等。这些操作对于对象在后续的使用过程中是必要的初始准备工作。
  • 提供多种初始化方式(通过重载):可以通过重载构造函数,提供不同的参数组合,从而支持多种对象初始化方式。例如,一个Rectangle类可以有一个默认构造函数来创建一个边长为特定默认值的矩形,也可以有一个带两个参数的构造函数来根据传入的长和宽创建矩形。

初始化:

方式1.函数引用

思考:类中的函数可以访问私有类型;通过类找函数,然后通过函数找私有变量

#include<iostream>
#include<string>
using namespace std;
class MM{
    int num;
public:
    int& getNum(){
        return num;
    }
    string& getName(){
        return name;
    }
//写一个输出
    void print(){
    cout<<num<<" "<<name<<endl;
    }
private:
    string name;
}
int main(){
    MM mm;
    mm.getNum() = 10;
    mm.getNum() = "love";
    
    mm.print();
}

 *注:这里先输出一个空格(由" "表示)

方式2.构造函数

·没有返回值

·名字与类名相同

·不需要自己调用,构造对象的时候被调用

·不写构造函数,存在一个默认的无参构造函数,写了,默认的不存在

·也可以函数缺省

#include<iostream>
#include<string>
using namespace std;
class MM{
    int num;
public:
    MM(int a, string b){
        num = a;
        name = b;
    }
    MM(){
        cout<<"我要用默认构造函数"<<endl;
    }

}
int main(){
    MM nn(10, "love")//构造函数的初始化方法
    nn.print();
}

主函数中,指针的初始化方法:

MM* a = new MM(11, "aa");
MM* b = new MM[2]{{22, "aa"}, {33, "aa"}}

b[0].print();
//先获取b指针向后移动一位后的所指向的对象,然后调用这个对象的print成员函数。
(*(b + 1)).print();

*注:最后还要进行指针的释放

delete[] arrPtr;
arrPtr = nullptr;

return 0;

使用delete[] arrPtr释放了这个数组所占用的内存,并将指针设置为nullptr

需要注意的是,动态分配的内存必须在适当的时候释放,否则会导致内存泄漏。同时,释放指针后应将其设置为nullptr,以防止意外地再次使用已释放的指针。

2.4拷贝构造函数

方式3.类引用

#include<iostream>
#include<string>
using namespace std;
class MM{
    int num;
public:
    MM(){}
    MM(int a, string b){
        num = a;
        name = b;
    }

    //拷贝构造函数,就是一个引用
    MM(MM& object){
        name = object.name;
        num = object.num;
    }

    void print(){
        cout<<num<<endl<<name<<endl;
    }
private:
    int num;
    string name;   
};
int main(){
    MM mm(10, "love");
    MM mm1 = mm;
    mm1.print();

    return 0;
}

拷贝构造函数是一种特殊的构造函数,它接受一个同类型的对象的引用作为参数,并用于创建一个新对象,新对象是对传入对象的一个副本。

触发条件:当使用一个已存在的对象来初始化另一个同类型对象时,例如在函数调用中以值传递方式传递对象,或者在容器中存储对象副本时,都会触发拷贝构造函数的调用。

       MM mm(10, "love");
       MM mm1 = mm; // 这里触发拷贝构造函数

这种初始化方式称为拷贝初始化。如果没有自定义的拷贝构造函数,C++ 编译器会自动生成一个默认的拷贝构造函数。默认的拷贝构造函数会进行成员变量的逐位复制,即将一个对象的每个成员变量的值直接复制到另一个对象的相应成员变量中。

资源管理

例如,如果一个类有一个指针成员变量指向动态分配的内存,默认的拷贝构造函数只会进行浅拷贝,即只是简单地复制指针的值,这会导致两个对象的指针指向同一块内存。当其中一个对象被销毁时,这块内存可能会被错误地释放,而另一个对象继续使用它,就会导致未定义行为。

通过自定义拷贝构造函数,可以实现深拷贝,即分配新的内存空间,并将原始对象的内容复制到新的内存中,确保每个对象都有自己独立的资源。

2.5析构函数

定义:

  • 在 C++ 中,析构函数是一种特殊的成员函数,用于在对象销毁时执行清理工作。它的名字是在类名前面加上一个 “~” 符号。例如,对于类MyClass,析构函数的名字是~MyClass()
  • 析构函数没有参数,也不能被重载(因为它的参数列表固定为无参数),每个类最多只能有一个析构函数。

什么时候需要写析构函数:当数据成员在动态申请(用指针)的时候需要手动写析构函数

#include<iostream>
#include<string>
#include<string.h>
using namespace std;
class MM{
public:
    MM(){}
    MM(char* a){
        name = new char[strlen(a) + 1];
        strcpy(name, a);
    }

    void print(){
        cout<<name<<endl;
        cout<<*name<<endl;
    }

//析构函数
    ~MM(){
        delete[] name;
        name = null;
        cout<<"调用析构函数"<<endl;
    }

private:
    char* name;  
};
int main(){
    MM mm("love")
    mm.print();

    return 0;
}

new char[length + 1]表示请求一块足够大的连续内存空间,用于存储字符数据。这块内存空间的大小是length + 1个字符的大小。

功能:

析构函数的主要目的是在对象被销毁时进行清理工作,释放对象在其生命周期中所占用的资源。例如,如果对象在其生命周期中  动态分配了内存、打开了文件或建立了网络连接  等,析构函数可以用来释放这些资源,以避免资源泄漏。

在这个例子中,默认构造函数为成员变量name动态分配了内存,而析构函数在对象被销毁时释放了这块内存。

*更多详细说明

1.析构函数的作用:
  • 释放资源
    • 当对象的生命周期结束时,析构函数会自动被调用,以释放对象占用的各种资源。最常见的资源是动态分配的内存。例如,如果一个对象在构造函数中使用new操作符分配了内存,那么在析构函数中就应该使用delete操作符来释放这块内存,避免内存泄漏。
    • 假设我们有一个简单的类String用于模拟字符串操作:(上文已举例)
  • 关闭文件或释放其他系统资源
    • 如果对象打开了文件、数据库连接或者其他系统资源,析构函数可以用来关闭文件或释放这些资源。例如,一个文件读取类可能在构造函数中打开文件,在析构函数中关闭文件:
class FileReader {
public:
    FileReader(const char* filename) {
        file = fopen(filename, "r");
    }
    ~FileReader() {
        if (file) {
            fclose(file);
        }
    }
private:
    FILE* file;
};
2.析构函数的调用时机

局部对象

  • 对于在函数内部定义的局部对象,当函数执行结束,对象离开其作用域时,析构函数会被调用。
void myFunction() {
    MyClass obj;
    // 对象obj的生命周期从定义开始,到函数结束
}
// 当myFunction函数结束时,obj的析构函数被调用

动态分配的对象

  • 当使用delete操作符显式删除一个通过new操作符动态分配的对象时,会调用该对象的析构函数。
MyClass* ptr = new MyClass();
// 一些操作
delete ptr;
// 调用ptr所指向对象的析构函数

 全局对象和静态对象

  • 全局对象和静态对象的析构函数在程序结束时被调用。全局对象的析构函数在main函数结束后,程序退出之前被调用。
  • 静态对象(包括静态局部对象和全局静态对象)的析构顺序与构造顺序相反,最后构造的静态对象最先析构。(指路static讲解中有)

2.6构造函数、析构函数的运行顺序问题

·先构造的对象后析构

·delete可以提早调用析构函数

示例:

·输出结果是AEEBCDDCBA,在main函数结束,即} 闭合时,这些对象被销毁,栈上的对象按照后进先出的顺序被销毁,析构函数中,打印语句,将销毁顺序打印出来

·nullptr是 C++ 11 引入的一个关键字,用于表示空指针。

·当一个指针被初始化为nullptr时,它清楚地表明这个指针当前没有指向任何有效的MM类型对象。在后续使用这个指针之前,需要对其进行赋值,使其指向一个合法的MM对象,这样可以帮助开发者更好地跟踪指针的状态,避免悬空指针(dangling pointer)问题。例如,在进行解引用操作(*pobject)之前,应该先确保pobject不是nullptr,否则会导致程序崩溃。

调用时机

1.默认构造函数在以下情况下被调用:

·当使用默认方式定义类的对象时,例如MyArrayClass obj;。

·当使用new关键字动态分配对象时,例如MyArrayClass* ptr = new MyArrayClass();。

2.析构函数在以下情况下被调用:

·当对象超出其作用域时,例如在局部作用域中定义的对象在离开该作用域时,析构函数会被自动调用。

·当使用delete关键字释放动态分配的对象时,例如delete ptr;,其中ptr是指向动态分配的MyArrayClass对象的指针。

2.7特殊的构造函数(初始化参数列表)

#include<iostream>
#include<string>
#include<string.h>
using namespace std;
class MM{

public:
    MM(int i_num, string str_name) :num(i_num), name(str_name){}

    void print(){
        cout<<num<<endl<<name<<endl;
    }

private:
    int num;
    string name;
};
int main(){
    MM mm(10, "love")
    mm.print();

    return 0;
}

输出:

10
love

三、const

3.1const数据成员

·只能采用初始化参数列表的方式初始化

·只能用不能修改

·必须要初始化

#include<iostream>
#include<string>
using namespace std;
class MM{

public:
    MM(int num, string name) :num(num), name(name){}

    void print(){
        cout<<num<<endl;
    }

private:
    const int num;
    string name;
};
int main(){
    MM mm(10, "love")
    mm.print();

    return 0;
}

它的作用是将传入的参数num初始化成员变量(后面黄色的那个)num,将参数name初始化成员变量name。对于num(num),因为num是一个常量成员变量,必须在初始化参数列表中进行初始化,不能在构造函数体内赋值。这里将传入构造函数的参数num的值用来初始化类中的常量成员变量num

3.2const成员函数

·const是放在函数后面

·常成员函数特性:在该函数不能修改数据成员

·常成员函数和普通函数可以共存

3.3const对象

·普通对象优先调用普通函数

·常对象只能调用常成员函数

示例:

#include<iostream>
#include<string>
using namespace std;
class MM{

public:
    MM(int num, string name) :num(num), name(name){}

    void print(){
        cout<<"我使用了普通函数"<<endl;
    }
    
    void print() const{
        cout<<"我使用了常函数"<<endl;
    }

    void max() const{
        cout<<"常函数求最大值"<<endl;
    }

    void min() {
        cout<<"常函数求最小值"<<endl;
    }

private:
    const int num;
    string name;
};
int main(){
//普通构造函数
    MM mm(10, "love")
    mm.print();mm.min();mm.max();//普通对象可以使用普通函数

//常函数
    const MM nn(10, "love");
    nn.print();nn.max();nn.min();//第三个nn标红——常对象不能使用普通函数,只能使用常函数

    return 0;
}

四、static 

4.1static数据成员

·类外初始化,初始化的时候不需要static修饰,要有类名限定

·静态数据成员他的调用不需要对象,但是依旧收到权限限定词限定

        1.例如,可以直接使用 “MM::size” 来访问MM类中的静态成员变量size,而不需要先创建一个MM类的对象再通过对象来访问。

        2.受到类的权限限定词   publicprivateprotected 的限制。

        静态数据成员被声明在private部分,那么在类的外部不能直接访问它。只能通过类的成员函数来间接访问。例如,在上面的代码中,如果将size声明为private,那么在main函数或其他外部函数中直接使用 “MM::size” 就会导致编译错误。

        如果声明为public,则可以在类的外部直接访问。

   protected限定的静态数据成员的访问规则与protected的非静态成员类似,在派生类中可以有一定条件地访问。

        ——这种权限限定的目的是为了实现信息隐藏和封装,确保类的内部实现细节不被随意访问和修改,提高程序的安全性和可维护性。即使静态数据成员具有全局性质的存储和访问方式,但通过权限限定词可以控制其 访问范围 ,使其符合面向对象编程的原则。

·静态成员是属于类的,是所有对象的公有

#include<iostream>
#include<string>
using namespace std;
class MM{

public:
    MM(){
        size++;
        cout<<"第"<<size<<"个用户"<<endl;
    }
    MM(int num) :num(num){
        size++;
        cout<<"第"<<size<<"个用户"<<endl;
    }
    static int size;

private:
    int num;
};

//static初始化--类型、类名、静态成员名
int MM::size = 0;

int main(){
    MM mm1;
    MM mm2;
    MM mm3;
    MM mm4(10);

    return 0;
}

*注:

·由于static是全局,是每个对象都有的,因此放在公有下面

静态数据成员:在类MM中,static int size;声明了一个静态数据成员size

--存储方式:静态数据成员独立于任何对象存在,不属于任何一个特定的对象。它被存储在全局数据区,不属于任何一个对象的内存空间。它在内存中只存在一份,无论创建多少个类的对象,静态数据成员都只有一个实例。

--初始化:在类外部进行初始化,格式为类型 类名::静态成员名 = 初始值;,这里是int MM::size = 0;。这种初始化方式确保了静态数据成员在程序开始执行时就被初始化,并且只初始化一次。

  1. 这种在类外部进行初始化的方式确保了静态成员变量的初始化在程序的全局范围内进行,而不是在每个对象的构造函数中重复进行初始化。
  2. 它为整个程序提供了一个与类MM相关的全局状态信息存储位置,方便在不同的地方访问和修改这个状态信息,而无需依赖于特定的对象实例。
  3. 体现了 C++ 中静态成员变量在管理与类相关的全局状态方面的强大功能,它可以在不增加对象存储开销的情况下,为整个类提供共享的状态信息。

--访问方式:可以通过类名和作用域解析运算符::直接访问,如MM::size

                    也可以通过类的对象访问,例如在构造函数中使用size++,但这种方式不太推荐,因为容易让人误以为静态数据成员是每个对象独有的。

4.2static成员函数

·函数前加static

·正常访问静态数据成员

·访问非静态数据成员:指定对象--->怎么指定,通过传参的方式

#include<iostream>
#include<string>
using namespace std;
class MM{

public:
    MM(){}
    MM(int num) :num(num){}

    void print(){
        cout<<size<<endl;
    }

  //static函数               //传参
    static void printStatic(MM mm){
        cout<<size<<mm.num<<endl;
    }
    static int size;

private:
    int num = 20;
};

int MM::size = 0;
int main(){
    MM mm1;

    mm1.print();

    mm1.printStatic(mm1);
    MM::printStatic(mm1);//不用对象,直接使用

    return 0;
}

输出:

020
020
020

*注(两点):

1,静态和非静态成员函数的差异

  • 非静态成员函数
    • 与类的对象实例紧密相关。当调用一个非静态成员函数时,编译器会隐式地传递一个this指针,这个this指针指向调用该函数的对象。
    • 这意味着非静态成员函数可以访问和操作对象的非静态数据成员,并且其行为可能因对象的不同而不同。
  • 静态成员函数
    • 属于整个类,而不是某个特定的对象。它没有this指针,因为它不依赖于对象的实例。
    • 静态成员函数只能访问类的静态数据成员和其他静态成员函数,不能直接访问非静态数据成员(除非通过对象来访问)

因此,不能够写成:

    void print(){
        cout<<size<<endl;
    }

    static void print(){
        cout<<size<<endl;
    }

 ·一个非静态成员函数print和一个静态成员函数print,它们的参数列表是相同的(这里都没有参数)。main函数中通过对象mm1.print()调用print函数时,编译器无法确定是要调用非静态的print函数还是静态的print函数。

·函数重载的目的是通过不同的参数列表来提供不同的函数行为,而不是通过函数是否是静态来区分,所以 C++ 不允许这种具有相同参数类型的静态和非静态成员函数重载。

2.静态成员函数的访问限制

在静态成员函数printStatic中,尝试输出静态成员变量size和非静态成员变量num。静态成员函数只能访问静态成员变量和其他静态成员函数,不能直接访问非静态成员变量。

这是因为静态成员函数不与特定的对象实例相关联,没有this指针指向一个具体的对象,所以无法确定要访问哪个对象的非静态成员变量。而静态成员变量是独立于任何对象实例存在的,所以可以在静态成员函数中直接访问。

可以通过传递对象作为参数给静态成员函数,然后通过该对象访问非静态成员变量。但这样就改变了静态成员函数的设计初衷,一般情况下不建议这样做。

4.3static对象

·更改生命周期

静态对象的生命周期延长

  • 当一个对象被声明为静态对象时,它的生命周期会改变。静态局部对象在程序执行第一次进入其定义所在的块时创建,并且在程序结束时才销毁,而不是在离开块的时候销毁。
  • 对于全局静态对象(在全局作用域中声明为静态),其生命周期从程序开始执行到程序结束。这使得静态对象可以在多个函数调用之间保持其状态,有效地延长了对象的生存时间,使其能够跨函数调用或者跨不同的代码块使用。

·静态对象第一个构造,最后构析,与放的位置无关

  • 静态对象会在普通非静态对象构造之前进行构造。例如,如果有一个类A的静态对象和一个类B的非静态对象,类A的静态对象会先构造。
  • 静态对象的析构顺序与构造顺序相反,并且是在程序结束时进行。最后构造的静态对象会最先析构。这保证了资源的正确释放顺序,特别是对于那些有依赖关系的静态对象。
  • 静态对象的构造和析构顺序是由它们在编译阶段确定的顺序决定的,而不是由它们在代码中的物理位置(例如在函数内部的前后顺序)决定。一旦编译完成,它们的构造和析构顺序就固定下来了,不会因为代码执行过程中的其他因素(如函数调用顺序等)而改变。这种特性使得静态对象在管理程序全局资源和状态方面具有很好的稳定性和可预测性。

五、this

*注:静态函数不能使用this

解释:

  • this指针:在 C++ 中,this指针是一个隐含的指针,它是一个指向当前对象的指针。当调用一个非静态成员函数时,编译器会自动将this指针作为一个隐藏的参数传递给该函数。
  • 例如,对于一个类MyClass中的成员函数void func(),在调用obj.func()(其中objMyClass的一个对象)时,编译器实际上会将obj的地址作为this指针传递给func函数,这样func函数内部就可以通过this指针来访问和操作obj的成员变量和成员函数。
  • 由于this指针是指向当前对象的,而静态成员函数没有与之关联的特定对象(它没有和某个具体对象绑定,所以不存在所谓的 “当前对象” 的概念),也就没有this指向的目标。

#include<iostream>
#include<string>
using namespace std;
class MM{
public:
    MM(){}
    MM(int num){
        this->num = num;
    }
    void print(){
        cout<<num<<endl;
//或
        cout<<this->num<<endl;
    }
private:
    int num = 20;
};

int main(){
    MM mm(10);
    mm.print();
    return 0;
}

六、类的组合

6.1构造函数的写法

·组合类必须采用初始化参数列表写法

·组合类必须调用分支类的构造函数

6.2类组合的构造析构顺序

·调用顺序组合组合类中的分支类对象定义的顺序有关,和初始化参数列表的写法无关

七、友元

详解:B站黑马视频指路

36 类和对象-友元-全局函数做友元_哔哩哔哩_bilibili

7.1友元函数

友元函数不是类的成员函数,但可以访问该类的私有成员和保护成员。

友元的目的 就是让一个函数或者类 访问另一个类中的私有成员

·类内在函数前声明friend

·声明后就可以当成结构体使用

*注:

·友元函数可以放在私有,也可以在公有下

·友元函数破坏了类的封装性,不安全

7.2友元函数作为另一个类的成员函数

7.3友元类

八、运算符重载

8.1<友元方式>+-*/

在 C++ 中,运算符重载函数的定义位置(成员函数或友元函数)和它的作用不同(如何与类的其他成员交互)。我们可以根据运算符的重载方式来判断它是成员函数还是友元函数。

1.+ 

友元函数是一种可以访问类的私有成员的非成员函数。友元函数通常用于实现运算符重载,特别是当需要访问两个操作数的私有成员时。由于是外部函数,它可以接受两个对象作为参数,并且不依赖于隐式 this 指针。

·operator<< 运算符通常被定义为一个友元函数,这样它可以直接访问类的私有成员,且它在类外部定义。关键是:友元函数是与类关联的,但它不是类的成员。

·友元函数的优势是,它能够像成员函数一样访问私有数据,但不需要通过 this 指针来访问。

详解:

MM operator+(MM object1, MM object2) {
    MM object;
    object.name = object1.name;
    object.grade = (object1.grade + object2.grade) / 2;
    return object;
}
  • 当执行object1 + object2时,会调用这个友元函数。在调用operator+函数时,object1object2是通过值传递的方式将它们的副本传递给函数
  • 这里object1object2是函数的局部副本。函数创建一个新的MM类对象object,并根据object1object2namegrade值来初始化object的成员变量。  (    它将objectname成员设置为object1name成员的值。这里object1object2是函数的参数,由于是友元函数,可以直接访问它们的私有成员。   )
  • 接着,它计算object1object2grade成员的平均值,并将结果设置为objectgrade成员的值。
  • 最后,函数返回新创建的MM类对象object,这个对象被赋值给main函数中的object3

 总结:友元函数operator+(MM object1, MM object2)在执行加法运算时,会创建一个新的MM类对象,将两个操作数的namegrade按照特定规则组合到新对象中,然后返回新对象。这个函数在main函数中通过对象相加的操作被调用。

*使用引用传递参数:

MM operator+(const MM& object1, const MM& object2) {
    MM object;
    object.name = object1.name;
    object.grade = (object1.grade + object2.grade) / 2;
    return object;
}

const关键字确保了object1object2在函数内部不能被修改。如果尝试在函数内部修改object1object2,编译器会报错。 

8.2<成员函数方式>

1.+

通过类的成员函数来实现+运算符重载

 详解:B站黑马指路39 类和对象-C++运算符重载-加号运算符重载_哔哩哔哩_bilibili

在这里,operator+ 是成员函数,定义在 Complex 类内部。operator+ 只能访问当前对象(通过 this以及作为参数传入的 other 对象的数据成员。运算符重载函数直接操作类的成员,因此我们在 operator+ 函数中用到了grade 和 name成员。

详解:

MM operator+(MM object)
  • objectoperator+函数的参数。当执行object1 + object2时,object1是调用operator+函数的对象(通过this指针隐式传递),object2是函数的参数。(在object1 + object2的情况下,object就是object2object.grade就是object2grade成员变量的值。)
MM mm((this->grade + object.grade) / 2, this->name);
return mm;
  • this指针指向调用operator+函数的对象。例如,object1调用operator+函数(即object3 = object1 + object2;),那么this指针指向object1this->grade表示object1grade成员变量,this->name表示object1name成员变量。
  • 函数计算(this->grade + object.grade) / 2,这里的objectoperator+函数的参数,即object2。然后使用计算结果和this->name创建一个新的MM类对象mm。 
  • 返回新对象(使用计算结果(this->grade + object.grade) / 2this->name来创建一个新的MM类对象mm)   最后,函数返回新创建的MM类对象mm,完成了+运算符的重载操作,使得两个MM类对象相加可以得到一个新的MM类对象。

*注:

成员函数访问私有变量的原理:

  • 在 C++ 中,类的成员函数可以访问类的私有成员变量。这是因为成员函数是在类的内部定义的,它们属于类的一部分。
  • 当一个函数是类的成员函数时,它隐式地传递了一个this指针,这个指针指向调用该函数的对象。通过this指针,成员函数可以访问对象的私有成员变量。
  • operator+函数中,正是通过this指针实现了对私有变量的访问。

另一种写法(等价的):

MM operator+(MM other) {
    MM result;
    result.name = this->name;
    result.grade = (this->grade + other.grade) / 2;
    return result;
}

这个是先创建对象再设置属性,而第一种方式是直接在构造函数中初始化对象。 

8.2.2小结

特性成员函数重载友元函数重载
定义位置定义在类内部定义在类外部,但声明为友元函数
第一个参数隐式是 this 指针,操作当前对象显式传入两个对象(操作两个对象的成员)
访问私有成员可以直接访问当前对象的私有成员可以访问友元函数声明所在类的私有成员
调用方式通过对象调用,如 obj.operator+()通过外部函数调用,如 obj1 + obj2

在 C++ 中,成员函数和友元函数都可以用来重载运算符,但它们的定义位置和调用方式不同:

  • 成员函数:通常适用于涉及当前对象的数据的操作,并且通过 this 指针访问对象的成员。适用于只涉及单个对象的数据时(因为它会隐式地使用 this 指针来访问对象的数据成员
  • 友元函数:适用于需要操作两个对象的数据,并且通过友元函数可以像外部函数一样灵活地访问类的私有成员。

8.3前置后置++重载

1.前置++重载

People& operator++() { }

class People {
public:
    int size;
    People& operator++() {
        this->size++;
        return (*this);
    }
};

int main() {
    People p;
    p.size = 5;
    ++p; // 调用前置自增运算符
    return 0;
}

实现了People类的前置自增运算符。当对People类的对象使用++运算符时,会增加对象的size成员变量的值,并返回自增后的对象本身。

2.后置++重载

  • 后置自增运算符的特点是先返回对象的当前值,然后再对对象进行自增操作。
  • People& operator++(int)函数中:
    • 首先创建了一个新的People对象object,并初始化其name"小爱"
    • 然后将新对象的size设置为当前对象的size
    • 接着对当前对象的size进行自增操作。
    • 最后返回新创建的对象。这符合后置自增的语义,即先返回原始值,再进行自增。

*在以上前置和后置操作中,staticd作用:

——1.static关键字在成员变量中的作用

当一个成员变量被声明为static时,它具有以下特性:

  • 共享性:所有该类的对象共享这个静态成员变量。这意味着无论创建了多少个类的实例,静态成员变量只有一份,存储在静态存储区。
  • 类级别的访问:静态成员变量可以通过类名直接访问,而不需要通过类的对象。例如,People::size是合法的访问方式。

——2.在前置自增和后置自增运算符重载函数中,都对这个size变量进行了操作。使用static来定义size是为了实现所有People类对象共享一个计数的功能,如果去掉static,将会改变代码的行为,使其失去这种共享计数的逻辑。

8.4流重载

为了更方便的输入输出,更多详解:

C++流插入和流提取的重载!_c++截获数据流-优快云博客

c++运算符重载与输入输出流重载_流输入运算符重载-优快云博客

输入输出流运算符重载函数一定要用友元类重载

1.输出

.cpp文件:

class MM{
//这个重载操作符定义了如何将MM类的对象输出到流中
    friend ostream& operator<<(ostream& out, MM& mm);

public:
    MM(){}
    MM(string name, int age) :name(name), age(age){}
private:
    string name;
    int age;
};

ostream& opertaor<<(ostream& out, MM& mm){
    out<<mm.name<<"年龄为:"<<mm.age<<endl;
    return out;
}

return out;:

  • 运算符重载的最后,返回了 out,即输出流对象。这是因为 << 运算符要求返回输出流对象,以便支持链式调用。
  • 例如,cout << obj1 << obj2 会先输出 obj1,然后继续输出 obj2

ostream& out 参数中的 &(传递输出流的引用):

  • 这里的 & 表示 out 是一个 引用,即 out 是一个指向 ostream 类型对象的引用,通常是指向输出流(例如 cout)。
  • 直接对原始的 ostream 对象进行操作。
  • ostream& out(参数中的引用):保证传递给 operator<< 函数的是原始的输出流对象(如 cout),而不是它的副本。这样可以直接修改原始对象(例如向 cout 中写入数据),避免不必要的复制开销——通过传递引用,operator<< 函数可以直接修改原始的输出流对象 out,并且返回给调用者,确保输出流能够继续传递。
  • ostream&(返回引用):允许返回传入的输出流对象的引用,使得我们可以连续进行多个输出操作(支持链式调用)。

头文件:

#include"标头.h"

int main(){
    MM mm("小美", 10);
    MM mm1("小爱", 11);
    cout<<mm   //out
        <<mm1; //out
}

控制台:

1.输入

class MM{

    friend istream& operator>>(istream& in, MM& mm);

public:
    MM(){}
    MM(string name, int age) :name(name), age(age){}
private:
    string name;
    int age;
};

istream& opertaor>>(istream& in, MM& mm){
    cout<<"输入名字和年龄(空格隔开)"<<endl;
    cin>>mm.name>>mm.age;
    return in;
}
MM mm2, mm3;
cin>>mm2>>mm3;
cout<<mm2<<mm3;
return 0;

控制台:

 

8.4.1return区别:打印重载

template<class type>
void print(type data){
    cout<<data;
}

若写成

return data;

 ·首先,返回类型是 void,但是函数尝试返回一个值 data,这与 void 返回类型不匹配。

·从 void 更改为 type,这样函数就可以返回传入的 data 值。但是,函数返回的是 data 的副本,而不是进行输出。

九、重载知识点

9.1基本知识点

 

9.2类重载

#include <iostream>
using namespace std;

class Point
{
private:
    int x;
public:
    Point(int x1)
    {
        x = x1;
    }
    Point(Point& p)
    {
        x = p.x;
    }
    Point operator+(Point& p); //使用成员函数重载加号运算符
    Point operator-(Point& p); //使用函数重载减号运算符
    Point operator*(Point& p);
    Point operator/(Point& p);
};

接上:

Point Point::operator+(Point& p)
{
    return Point(x + p.x);
}

Point Point::operator-(Point& p)
{
    return Point(x - p.x);
}

Point Point::operator*(Point& p)
{
    return Point(x * p.x);
}

Point Point::operator/(Point& p)
{
    return Point(x / p.x);
}

int main()
{
    Point a(1);
    Point b(2);
    a + b; //正确,调用成员函数
    a - b; //正确,调用友元函数
    a / b;
    a * b;
}

9.3<<和>>重载

// <<和>>重载
#include <iostream>
using namespace std;

class C
{
public:
    C()
    {
        r = 0;
        i = 0;
    }
    C(double r, double i) : r(r), i(i) {}
    // 类重载
    friend ostream& operator<<(ostream& output, C &c);
    friend istream& operator>>(istream& input, C &c);
private:
    double r;
    double i;
    char op, ch;
};

// 重载>>运算符
istream& operator>>(istream& input, C &c)
{
    // 1+2i
    // op:保存+, ch保存i
    input >> c.r >> c.op >> c.i >> c.ch;
    return input;
}

// 重载<<运算符
ostream& operator<<(ostream& output, C &c)
{
    output << c.r << c.op << c.i << c.ch;
    return output;
}

接上:

int main()
{
    C c1, c2;
    cout << "输入一个复数";
    cin >> c1;
    cout << "输入第二个复数";
    cin >> c2;
    cout << "C1= " << c1;
    cout << "C2= " << c2;
    return 0;
}

9.4友元重载

#include <iostream>
using namespace std;

class Point
{
private:
    int x;
public:
    Point(int x1)
    {
        x = x1;
    }
    Point(Point& p)
    {
        x = p.x;
    }
    friend Point operator+(Point& p1, Point& p2);
    friend Point operator-(Point& p1, Point& p2);
    friend Point operator*(Point& p1, Point& p2);
    friend Point operator/(Point& p1, Point& p2);
};

Point operator+(Point& p1, Point& p2)
{
    return Point(p1.x + p2.x);
}

Point operator-(Point& p1, Point& p2)
{
    return Point(p1.x - p2.x);
}

Point operator*(Point& p1, Point& p2)
{
    return Point(p1.x * p2.x);
}

Point operator/(Point& p1, Point& p2)
{
    return Point(p1.x / p2.x);
}

接上:

int main()
{
    Point a(1);
    Point b(2);
    a + b;
    a - b;
    a / b;
    a * b;
}

9.5流函数<<和>>重载

// 流函数<<和>>重载
#include <iostream>
using namespace std;

class Point
{
private:
    int x;
    int y;
public:
    Point(int x1, int y1)
    {
        x = x1;
        y = y1;
    }
    void output(ostream& out);
    void input(istream& in);
    friend ostream& operator<<(ostream& out, Point& p);//使用友元函数重载<<
    friend istream& operator>>(istream& in, Point& p);//使用友元函数重载>>
};

*直接完成:

ostream& operator<<(ostream& cout, Point& p)
{
     cout << p.x << " " << p.y << endl;
     return cout;
 }

istream& operator>>(istream& cin, Point& p)
{
     cin >> p.x >> p.y;
     return cin;
 }

接上上: 

void Point::output(ostream& out)
{
    out << x << " " << y << endl;
}

void Point::input(istream& in)
{
    in >> x >> y;
}

ostream& operator<<(ostream& out, Point& p)
{
    p.output(out);
    return out;
}

istream& operator>>(istream& in, Point& p)
{
    p.input(in);
    return in;
}

int main()
{
    Point a(1, 2);
    Point b(2, 3);
    cin >> a >> b;
    cout << a << b << endl;
}

*补充:

在 C++ 中,当我们需要重载 << 运算符以实现自定义类型的输出时,友元函数成员函数的使用方式有一定的区别,尤其是在处理 structclass 时。

structclass 的唯一区别是默认的成员访问权限不同:

  • struct 的成员默认是 public,即可以从外部直接访问。
  • class 的成员默认是 private,即不能直接从外部访问。

1. 友元函数和成员函数

  • 友元函数(friend function): 可以访问类的私有成员和保护成员。对于重载 << 运算符,一般建议将其实现为友元函数,因为输出流操作通常需要访问类的内部数据成员,而将它实现为友元函数可以确保它不需要成为类的成员

  • 成员函数: 成员函数是属于类的函数,它只能访问该类的成员(包括私有和保护成员)。如果将 << 运算符重载为成员函数,std::ostream 需要作为左操作数(即输出流)传入,而成员函数的第一个参数通常是隐式的 this 指针(需要显式传递给成员函数,通过对象调用该函数(例如 obj.operator<<(cout))),而不能像通常的运算符那样使用 cout << obj 形式。

  • 因此,<< 运算符重载为成员函数时,它的使用方式较为局限,且不能像友元函数一样灵活。

十、继承派生知识点

1.类的组合的例子

 2.类继承的例子:

继承与派生:

继承:子继父业,原模原样

派生:继承之后,做大做强

继承与派生实例:

左侧:

  • 继承与派生实例:交通工具的继承关系
    • 在图的左侧,有一个完整的继承与派生实例。
    • 基类(父类):交通工具(Vehicle)
    • 派生类(子类):包括火车(Train)、汽车(Car)、飞机(Airplane)、轮船(Ship)等。
    • 这些派生类进一步派生,例如汽车类派生了卡车(Truck)、旅行车(Travel Car)、小汽车(Small Car)等。小汽车类又派生了工具车(Utility Vehicle)、轿车(Sedan)和秒包车(Second - hand Package Car)。
    • 这种层次结构展示了继承的多级派生。每一级派生类都继承了上一级类的属性和方法,并可以添加自己的特定属性和方法。例如,交通工具类可能有 “移动” 方法,汽车类在继承这个方法的基础上,添加了 “四轮驱动” 属性,而小汽车类在继承汽车类属性和方法的基础上,可能添加了 “适合城市驾驶” 的属性。

右侧: 

1. 单继承

  • 单继承示例:汽车的继承关系
    • 在图的右侧,有一个单继承的示例。
    • 基类(父类):汽车(Car)
    • 派生类(子类):小汽车(Small Car)和大汽车(Large Car)
    • 这里,小汽车和大汽车都继承自汽车类。它们继承了汽车类的属性和方法,并且可以在此基础上添加自己的特定属性和方法。例如,汽车类可能有 “行驶” 方法,小汽车和大汽车在继承这个方法的同时,可能会有自己独特的属性,如小汽车可能有 “停车方便” 的属性,大汽车可能有 “载货量大” 的属性。

2. 多继承

  • 多继承示例:助教博士的继承关系
    • 在图的右下角,有一个多继承的示例。
    • 基类(父类):学生(Student)和老师(Teacher)
    • 派生类(子类):助教博士(Teaching Assistant Doctor)
    • 这里,助教博士类继承自学生类和老师类。它继承了学生类和老师类的属性和方法。例如,学生类可能有 “学习” 方法,老师类可能有 “授课” 方法,助教博士类在继承这些方法的同时,可能会有自己独特的属性,如 “协助教学” 和 “进行科研”。

10.1继承与派生

10.2单继承

10.3权限限定在继承中的作用

class Human{  //基类
public:
    int a = 1;
protected:
    int b = 2;
private:
    int c = 3;
};

//定义了一个名为MM的类,这个类以公有继承(public)的方式从Human类派生而来。
class MM :public Human{
public:
    void aa(){
        cout<<a<<endl;
        cout<<b<<endl;
        //cout<<c<<endl;  //c是不可访问的,继承的私有不可被访问
    }
};
//以保护继承(protected)的方式从Human类派生而来。
class Boy :protected Human{
    public:
    void aa(){
        cout<<a<<endl;
        cout<<b<<endl;
        //cout<<c<<endl;  //同样c是不可访问的,继承的私有不可被访问
    }
}

.cpp文件:

int main(){
    MM mm;
    mm.aa();

    return 0;
}

 输出:

1
2

访问权限方面 (protect)

  • MM类外部无法直接访问这些原本在Human类中是公有的成员,而只能在MM类及其派生类的成员函数内部访问。

继承关系的含义方面 (public 与 protect)

  • protected继承主要用于表示一种 “实现继承”,即派生类继承基类的实现细节主要是为了自身的实现或者其派生类的实现,而不是为了向外部提供和基类一样的接口。
  • 相比之下,public继承更像是一种 “接口继承”,派生类可以在保持基类接口的基础上进行扩展,外部用户可以像使用基类对象一样使用派生类对象(对于公有成员而言)。

代码的可维护性和扩展性方面 (public 与 protect)

  • 使用protected继承会使代码的封装性更强。因为它限制了外部对从基类继承来的成员的访问。
  • 但是,这也可能会使代码的使用稍微复杂一些,因为外部代码如果需要访问原来基类中的某些功能,可能需要通过派生类提供的其他接口(成员函数)来间接访问,而不能像public继承那样直接访问。例如,要访问Human类中的某个功能,在public继承下可以直接通过MM类对象访问,而在protected继承下可能需要在MM类中定义新的公有成员函数来间接调用。

10.4小知识点

 

十一、继承的构造问题

11.1继承构造

只能用初始化列表的方式初始化继承的构造函数

---->先是构造基类的MM,再是继承的Son

11.2连续继承构造

输出:

1 2 3
1 2 3

简单叙述:

 类C

  • 公有继承自类 B(class C : public B
  • 有一个公有成员变量c和一个公有构造函数C(int a, int b, int c) : B(a, b), c(c){}  该构造函数用于 初始化从类 B 继承的成员变量  a和  ,以及自身的成员变量 c
  • 有一个公有成员函数void print(),用于输出成员变量abc的值。

 主函数

        创建类 C 的对象c时,首先调用类 A 的构造函数,然后调用类 B 的构造函数,最后调用类 C 的构造函数。这是因为类 C 继承自类 B,类 B 继承自类 A。

        这种继承结构有助于代码的复用和扩展。

11.3多继承构造

输出:

简单叙述:

Son

  • Son 类继承了 MMBoy,并包含一个构造函数,接受六个参数:mmNameboyNamesonNamemmAgeboyAgesonAge
  • Son 类的构造函数中,首先调用了 MM 的构造函数 MM(mmAge, mmName)Boy 的构造函数 Boy(boyAge, boyName) 来初始化父类成员。
  • 然后,通过 this->sonAge = sonAgethis->sonName = sonName 来初始化 Son 类特有的成员变量。

打印函数

  • Son 类中定义了一个 print() 函数,用于输出 sonAgesonName,格式为“年龄:姓名”

改进:

更优的做法是在构造函数的初始化列表中直接初始化成员变量,而不是在构造函数体内赋值。这样可以避免默认构造和赋值的双重操作,提升性能和代码效率。

public:
    Son(string mmName, string boyName, string sonName, int mmAge, int boyAge, int sonAge)
        : MM(mmAge, mmName), Boy(boyAge, boyName), sonAge(sonAge), sonName(sonName) {
        // 构造函数体可以为空
    }

11.4继承中的使用函数问题<就近原则>

输出:

输出分析(就近原则):

        继承结构中的函数调用遵循就近原则,也就是说,如果派生类(Son)中重写了基类(MM)的函数,则调用派生类的函数,而不是基类的函数。

        执行 mm.print() 时,调用的是 MM 类的 print() 函数;而执行 son.print() 时,由于 Son 类中重写了 print() 函数,调用的是 Son 类的 print() 函数——而执行 son.print() 时,由于 Son 类中重写了 print() 函数,调用的是 Son 类的 print() 函数.

11.5虚继承

引入:

(考察构造顺序与析构顺序)

输出:

ABCDDCBA

*构造顺序只和继承顺序有关,跟初始化参数列表构造顺序无关

因此不应该是构造的方法:ABACAD

调换继承顺序查看:

输出:

ACBDDBCA

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值