C++ 复习记录(个人记录)

1、构造函数(constructor)是什么

答:类里面定义一个函数, 和类名一样, 这样在我们生成一个对象之后,就会默认调用这个函数,初始化这个类。

子类B继承父类A的情况, 当你调用子类的对象,构造函数会怎么样运行?

  • 1、创建子类对象时,程序首先调用父类的构造函数,然后再调用子类的构造函数

    • 父类构造函数负责初始化继承的数据成员
    • 子类构造函数主要用于初始化新增的数据成员
  • 2、子类构造函数总是需要调用一个父类构造函数;默认就是调用父类无参数构造函数,如果父类没有这个构造函数,程序会默认生成一个无参数构造的,如果父类又 有参数构造函数,但是没有无参数的构造函数,就会报错。

    • 所以当父类没有无参数的构造函数时,就必须明确指明调用哪一个构造函数,需要用到初始化列表这个方式。
       Person(int age, char *address): age(age), address(address)

2、final用于禁止被重写和继承,确保自己的唯一,是最后一个。

  • 1 、禁止类自己被继承
    final 用于禁止 虚函数被重写和 类被继承 class Person final {} ,然后这个Person就不可以被继承了,
  • 2、 一个类, 自己继承了别人后,禁止别人继承自己。
class Person {
    
};


class Student final: public Person {
    
};

class Middel_School_Student : public Student{  // 失败,因为父类Student是final修饰的
    
};

就是意味着继承了Person。但是Student不能被别人继承。

  • 3、final用于禁止函数被重写
class Person { 
    virtual void run() final {  // 表示该方法是最终方法,无法被重写  
    }
};

class Student : public Person{
    
    // 编译错误,方法无法被重写。
    void run(){ 

    }
};

final总是放在定义类的后面,和方法的后面, 类是 class A final {}. 和 class A :final public B {}.方法是

class Person {  
    virtual void C() final {  // 表示该方法是最终方法,无法被重写     
    }
};

也是在后面,但是在()前。

3、 友元 friend ,目的是为了使用私有属性的函数和变量。 类似一个白名单

如果一个类A里面有私有的方法和属性,当类生成一个对象 a ,肯定是无法调用这些方法和属性的,如果在类A里面,加上 友元函数 或者友元类 之后,这个友元函数内部可以无视private的影响。 友元类里也可以无视private的影响。

5、重载

C++中,一个类里,如果有两个函数名一样的函数,但是入参不同,就属于重载

4、关于 继承

1、继承是什么
继承就是把一个类里的变量和方法 “继承” 给另一个类。如果B类 继承 A类,我们就叫B类是子类, A类是B类的父类,B类有A类的所有功能和方法(先这样粗浅的理解)。

2、继承的写法
继承有三种方式 pubulic prtected private 和类的封装时候的三种方式一样。不写默认就是private

class 子类名:[继承方式]父类名{    
  子类新增加的成员    
};

2、继承的用法

  • 子类继承父类之后,拥有了一套和父类一样的变量和方法,即可以使用父类的方法和变量的,如果想要使用父类中继承下来的变量和函数。就在子类中使用 this-> 【方法和变量】 ,就可以调用。

3、继承需要注意

  • 类本身是一个抽象的概念,类本身不占用内存空间;它仅仅是一个蓝图或模板,用于创建对象。只有当类被实例化为对象时,才会分配内存空间。每个对象都有其独立的地址。所以当我们用类生成对象时,每个对象都是一个独立的个体。

  • 比如:如果C++中,一个类B,继承于类A, 则类A的对象中的变量var_a, 在类B的对象中也会有,如果类B生成一个对象obj_b, 那么这个对象obj_b里面也有var_a。 这个时候,使用类A的方法调用var_a和 使用类B的方法调用var_a,都可以调用到var_a, 并且这两个var_a是同一个,即地址是一样的。

#include <iostream>

using namespace std;


class A {
public:
    int var_a = 4;

    void get_addr() {
        cout << "IN A : var_a  ==" << var_a << "\n";
        cout << "IN A : var_a address ==" << &var_a;

    }
};

class B : public A {
public:
    int var_b;

    void get_addr1() {
        cout << "IN B : var_a  ==" << var_a << "\n";
        cout << "IN B : var_a address ==" << &(this->var_a) << "\n";
    }
    void get_addr2() {

        A::get_addr();
    }

};


int main() {

    B obj_b;
    obj_b.var_a = 10;
    //打印在对象obj_b里, 调用类B变量 var_a的值和地址
    obj_b.get_addr1();
    
    cout<<"================================================="<<"\n";
    
    //打印在对象obj_b里, 调用类A变量 var_a的值和地址
    obj_b.get_addr2();

    return 0;
}

  • 4、继承之后怎么用( 等重写,多态知道了之后再看)

继承之后,子类的对象,自然可以调用父类的一切(在子类的函数中,调用从父类继承而来的成员变量或成员函数,直接使用this),那么父类的对象,可以调用子类么 ?

  1. 子类对象可以调用父类的方法
    当一个类(子类)继承自另一个类(父类)时,子类对象可以调用从父类继承来的所有公有(public)和保护(protected)成员(包括方法和属性)。如果父类的方法或属性是私有的(private),则子类无法直接访问它们,但可以通过父类的公有或保护方法来间接访问。

  2. 父类对象不能调用子类的方法
    父类对象只能调用自己定义的公有和保护成员,不能调用子类特有的方法或属性。这是因为父类对象在内存中的布局并不包含子类的任何额外成员或方法。换句话说,父类对象对子类扩展的部分一无所知。

  3. 多态性
    虽然父类对象不能直接调用子类的方法,但可以通过指向子类对象的父类指针或引用来实现多态性。这通常是通过在父类中定义虚函数(virtual function)来实现的。当子类重写了父类的虚函数时,通过父类指针或引用调用的将是子类版本的函数。

示例代码:

#include <iostream>

class Parent {
public:
    void parentMethod() {
        std::cout << "Parent method called" << std::endl;
    }

    virtual void virtualMethod() {
        std::cout << "Parent virtual method called" << std::endl;
    }
};

class Child : public Parent {
public:
    void childMethod() {
        std::cout << "Child method called" << std::endl;
    }

    void virtualMethod() override {
        std::cout << "Child virtual method called" << std::endl;
    }
};

int main() {
    Parent parent;
    Child child;

    // 子类对象调用父类方法
    child.parentMethod();

    // 子类对象调用自己的方法
    child.childMethod();

    // 通过父类指针调用子类重写的虚函数
    Parent* parentPtr = &child;
    parentPtr->virtualMethod();  // 输出:Child virtual method called

    // 父类对象不能调用子类方法
    // parent.childMethod();  // 编译错误

    return 0;
}

在这个例子中:

  • child 对象可以调用 parentMethod()childMethod()
  • 通过父类指针 parentPtr 调用 virtualMethod() 时,实际调用的是子类的 virtualMethod(),展示了多态性。
  • 尝试通过父类对象 parent 调用 childMethod() 会导致编译错误,因为父类对象不知道子类的任何扩展。

总结:子类对象可以调用父类的公有和保护成员,但父类对象不能调用子类的成员。多态性允许通过父类指针或引用来间接调用子类的重写方法。

4 、 那么 ,是不是通过父类指针,调用子类的成员,一定是父类也拥有的成员才可以?

是的,通过父类指针调用子类的成员时,只能调用父类也拥有的成员。这是因为多态是基于父类指针或引用来实现的,编译器在编译时期会根据指针或引用的类型来确定要调用的成员函数。如果子类有额外的成员,且这些成员在父类中没有对应的声明,那么通过父类指针或引用是无法调用这些成员的。

5、重写
  • 1、重写就是子类继承父类之后,子类中,定义了父类相同名字的函数。

重写之后,子类直接调用,就是调用的子类自己的函数。 父类中所有的同名的函数(为啥说所有,是因为父类中可能有重载的函数,所以会有同名的函数 ),都被子类的同名函数覆盖了,即父类中所有的同名函数,子类都不会继承,子类只用自己重写的那个,如果你想在子类中调用这个被你重写的,就需要用父类名去调用 【父类名】::【父类的重写的方法】 。

#include <iostream>

using namespace std;


class A {
public:

    void display() {
        cout << "A::display()" << "\n";
    }
    // 1、重载一个display函数
    void display(int num) {
        cout << "A::display(int num)" << "\n";
    }
};

class B : public A {
public: 
    // 2、重写一个display. 重写之后,自然而然的,父类的void display() 方法被顶替了,并且 void display(int num)也被顶替了(重载的全被顶替了)
    // 综上 父类的两个display都不不能直接调用了。
    void display() {
        cout << "B::display()" << "\n"; 
        //3、 this->display(100);  // 编译失败,原因如上面解释的一样 ,但是如果想使用父类的函数也是可以的, 方法1,需要用父类,类A来调用方式如下

        cout <<"---------------------------------------------------------"<< "\n";

        A::display(); 
        A::display(100); 
    }

};

int main() { 
    class B b; 
    b.display();  // 调用子类的函数 
    //4、同2表示的一样 
    // b.display(100);  // 编译失败 

    return 0; 
}

在这里插入图片描述

  • 2、重写 中 override 关键字,override 加在函数名的()后, {}前

在继承关系下,子类可以重写父类的函数,但是有时候担心程序员在编写时,有可能因为粗心写错代码。所以在C++ 11中,推出了 override 关键字,用于表示子类的函数就是重写了父类的同名函数 。 不过值得注意的是,override 标记的函数,必须是虚函数。
在这里插入图片描述

6、 多层继承和多继承
  • 多层继承(Hierarchical Inheritance)就是 继承了 很多层,比如 爷爷继承给父亲,父亲继承给儿子,继承了3层。
  • 多继承(Multiple Inheritance)就是 同时继承了多个, 比如 父亲母亲,同时继承给儿子。
7、 多态

1、多态就是,有多个相同名字的函数,当调用时,会根据调用时的方式不同,调用不同的函数。比如一个人,同样是说话这个行为,对有的人态度好,对有的人态度差,展现了不同的“态度”

2、C++中通过 visual (虚函数)来实现多态。

  • 通过 virtual可以实现真正的多态

    virtual (虚函数 )可以在父类的指针指向子类对象的前提下,通过父类的指针调用子类的成员函数
    这种技术让父类的指针或引用具备了多种形态,这就是所谓的多态

  • 最终形成的功能:

    • 如果父类指针指向的是一个父类对象,则调用父类的函数
    • 如果父类指针指向的是一个子类对象,则调用子类的函数

3、怎么使用 visual

  • 定义 visual ( 虚函数) 非常简单,只需要在函数声明前,加上 virtual 关键字即可
    • 在父类的函数上添加 virtual 关键字,可使子类的同名函数也变成虚函数,可以子类父类都写上visual
#include<iostream> 

using namespace std; 

class Father { 
public: 
    virtual void show() { 
        cout << "father show" << endl; 
    }
};

class Children : public Father { 
public: 
    virtual void show() { 
        cout << "children  show" << endl; 
    }
};

int main() { 
    Father *father = new Father(); 
    father->show();  // 调用父类的show函数 

    Children *children = new Children();
    children->show();  // 调用子类的show函数

    Father *p = new Children();
    p->show();  // 调用哪个类中的show函数? 就看父类的指针,指向了哪个对象,指向的是子类,所以调用的就是子类的函数

    return 0;
}

在这里插入图片描述

总结: 多态需要以下三个条件
1、有继承关系
2、子类重写了父类的函数
3、定义一个父类的指针变量,指向子类调用子类,指向父类,调用父类。

8、 初始化列表的前序
  • 初始化列表它是构造函数的一部分,可以说是"定制化的构造函数"。因为,虽然构造函数体执行之前也有 初始化成员变量的操作。但是构造函数是有默认的逻辑的,很多时候,默认的构造函数初始化逻辑并不符合我们的需求。例如,我们可能希望成员变量使用特定的值进行初始化,或者我们希望调用某个特定的构造函数来初始化它们。

如果使用初始化列表来指定构造函数:

我们知道的是,构造函数是类生成一个对象之后,自动调用来初始化的函数,如果一个类A里有一个 构造函数,类B继承了类A,那么当类B生成一个对象后,有几种情况。

1、类B在初始化一个对象之后,会调用A里面这个构造函数么 ?
答:会
2、类B 里面 如果有一个自己的构造函数,类B的对象会调用哪个构造函数呢 ?
答:都会调用,且顺序是先调用父类的构造函数,即 A 里面的构造函数,再调用B里面的构造函数。

#include <iostream> 

using namespace std; 

class A { 
public: 
    A() { 
        cout << this << "父类A的 无参数构造函数被调用\n";
    }
};

class B : public A {
public:
    B() {
        cout << this << "父类B的 无参数构造函数被调用\n";
    }
};

int main( ) {   
    B obj_1;   
    return 0;   
}

在这里插入图片描述

3、构造函数在类中,可以有多个,区别只是参数不同,那么我们默认的调用顺序是什么呢?

  • 首先还是先调用父类的构造函数,再调用子类的构造函数
  • 当父类有 无参构造函数 时,在自动调用子类构造函数之前,一定会自动调用父类的无参构造函数
  • 父类没有无参构造函数,那么子类必须指定父类调用哪一个,即使父类只有一个带参数的构造函数,否则编译会报错。这个和子类调用自己的构造函数不同,调用自己的还是和单个类生成对象一样, 即 :看传入的参数和哪个构造函数入参类型一样
//当父类有无参构造函数时,在自动调用子类构造函数之前,一定会自动调用父类的无参构造函数
#include <iostream> 

using namespace std; 

class A { 
public: 
    A() { 
        cout << this << "父类A的 无参数构造函数被调用\n"; 
    }

    A(int temp) { 
        cout << this << "父类A的 有参数构造函数被调用\n"; 
    }
};

class B : public A {
public:
    B() {
        cout << this << "父类B的 无参数构造函数被调用\n";
    }

    B(int temp) {
        cout << this << "父类B的 有参数构造函数被调用\n";
    }
};

int main() {
    B obj_1;
    cout << "---------------\n";
    B obj_2(11);
    return 0;
}

在这里插入图片描述

//父类没有***无参构造函数***,那么子类**必须指定**父类调用哪一个,即使父类只有一个带参数的构造函数,否则编译会  报错 Constructor for 'B' must explicitly initialize the base class 'A' which does not have a default constructor “B”的构造函数必须显式初始化没有默认构造函数的基类“A”

#include <iostream>

using namespace std;

class A { 
public:
//    A() {
//        cout << this << "父类A的 无参数构造函数被调用\n";
//    }

    A(int temp) {
        cout << this << "父类A的 有参数构造函数被调用\n";
    }
};

class B : public A {
public:
    B() {
        cout << this << "父类B的 无参数构造函数被调用\n";
    }

    B(int temp) {
        cout << this << "父类B的 有参数构造函数被调用\n";
    }
};

int main(int argc, char **argv) {
    B obj_1;
    cout << "---------------\n";
    B obj_2(11);
    return 0;
}

在这里插入图片描述

4、C++为什么要设计成,子类对象生成的时候,父类A中的构造函数也会被调用呢?

简单点理解,举个例子,如果父类中如果有private修饰的成员变量的话,在子类中是无法被调用的,也就是意味着子类不能够对这些private成员变量进行初始化,因此就需要调用父类的构造函数

AI解释一下:
C++设计成在子类对象生成时,父类(或基类)A中的构造函数也会被调用的原因,主要基于面向对象编程(OOP)的继承机制和对象构造的逻辑。以下是对这一设计的详细解释:

  1. 对象构造的完整性
    在C++中,一个子类对象是由其基类部分和自身特有的部分组成的。为了确保子类对象在构造时能够完整地表示其应有的状态,基类部分必须被正确初始化。这包括基类成员变量的初始化和基类构造函数中可能执行的任何其他初始化操作。

  2. 资源管理和依赖关系
    基类构造函数通常负责初始化基类对象所需的资源,如内存分配、文件打开、网络连接等。如果基类构造函数不被调用,那么这些资源可能不会被正确分配或初始化,从而导致资源泄漏、访问违规或程序崩溃。此外,基类构造函数还可能设置基类对象与其他对象之间的依赖关系,这些关系在子类对象中也必须得到维护。

  3. 多态性和接口一致性
    C++支持多态性,允许通过基类指针或引用来操作派生类对象。为了确保这种操作的安全性,基类部分必须被正确初始化。如果基类构造函数不被调用,那么通过基类接口访问派生类对象时可能会遇到未定义的行为。此外,基类构造函数还可能提供了一组通用的初始化逻辑,这些逻辑在派生类中应该被保留和扩展。

  4. 构造顺序和初始化链
    C++规定了对象的构造顺序:首先调用基类的构造函数(如果有多个基类,则按照它们在派生类中的声明顺序依次调用),然后按照成员变量的声明顺序调用成员变量的构造函数(如果有的话),最后调用派生类的构造函数。这种顺序确保了对象在构造时的逻辑一致性和可预测性。

  5. 代码重用和减少冗余
    如果每个派生类都需要手动复制基类的初始化代码,那么这将导致大量的代码冗余和难以维护。通过自动调用基类构造函数,C++允许派生类重用基类的初始化逻辑,从而简化代码并提高可维护性。

综上所述,C++设计成在子类对象生成时自动调用父类构造函数,是为了确保对象构造的完整性、正确管理资源、维护多态性和接口一致性、保证构造顺序的逻辑一致性以及减少代码冗余和提高可维护性。这是面向对象编程中继承机制的一个基本特性,也是C++语言设计的一个重要方面。

总结:

  • 创建子类对象时,程序首先调用父类的构造函数,然后再调用子类的构造函数

    • 父类构造函数负责初始化继承的数据成员

    • 子类构造函数主要用于初始化新增的数据成员

  • 子类构造函数总是需要调用一个父类构造函数;当父类没有无参数的构造函数时,就必须显式指明调用哪一个构造函数

5、怎么显示的指明调用哪个构造函数呢,就需要用到 初始化列表 了。

9、 初始化列表怎么调用的
  • 初始化列表位于构造函数的参数列表之后,用冒号(:)开头,后面跟着以逗号分隔的成员变量列表,每个成员变量后面跟一个放在括号中的初始值或表达式。

    • 1、单个类里调用初始化列表
#include <iostream>  

using namespace std;  

class Person {  
public:  
    int age;  
    char *address;  

//    // 这是普通的写法  
//    Person(int age, char *address) {  
//        this->age = age;  
//        this->address = address;  
//    }  

    // 使用初始化列表,对成员变量进行设置默认值  
    Person(int age, char *address): age(age), address(address){    }
};


int main(int argc, char **argv) {  
    // 创建对象  
    Person boy(33, "shanghai");  
    // 获取对象的成员变量  
    cout << boy.age << " " << boy.address << "\n";  

    return 0; 
}

在这里插入图片描述

  • 2、 调用父类的构造函数
    在这里插入图片描述

之前这个图,父类的构造函数里,没有无参数构造函数,而我们有没有指定构造函数是哪个,所有出现了这样的报错提示,我们现在就可以通过 初始化列表 来解决这个问题。
格式就是 构造函数的参数列表之后,以冒号(:)开头

#include <iostream>

using namespace std;

class A {
public:
    A(int temp) {
        cout << this << "父类A的 有参数构造函数被调用\n";
    }
};

class B : public A {  
public:  

    B(int temp) :A(13){  
        cout << this << "父类B的 有参数构造函数被调用\n";  
    }
};

int main(int argc, char **argv) {  
//    B obj_1;  
    cout << "---------------\n";  
    B obj_2(11);  
    return 0;  
}

10、泛型编程
  • 1、比如下面这个程序,函有两个名为add的函数,它们的功能几乎相同,只是处理的数据类型不同。一个处理int类型,另一个处理float类型, 所以我们去泛化这个数据类型,让这个数据类型变得不固定 。 泛型编程,就是把这个int 和float给"泛化",不让它成为一个固定的数据类型。这个也类似于我们 面向对象的类,类启到了一个模板的作用。
#include <iostream>   

using namespace std;   

int add(int a, int b) { //把两个int类型相加返回一个int    
    return a + b;   
}

float add(float x, float y) { //把两个float 类型相加,返回一个float

    return x + y;   
}

int main() {   

    int result = add(3, 4);   
    cout << "result  = " << result << endl;   

    float result2 = add(3.5f, 4.5f);   
    cout << "result2 = " << result2 << endl;   

    return 0;   
}

  • 2、泛型编程格式
  • 格式就是 写一个 template <typename T> ,下面加一个函数。 T 可以换成别的字母,这种格式就用的是模板(Templates),模板是一种将类型参数化的工具,可以用来定义通用的类、函数和算法。通过使用模板,可以编写可以适用于多种数据类型的代码,C++ 提供了函数模板(Function Templates)和类模板(Class Templates)来支持泛型编程。
  • 需要注意的就是 一个 template <typename T> 只能泛化一个函数模板,这个函数就这一样下面的一个函数模板。
  • 这个T是什么类型,是在编译的时候的时候,确认实际用的是什么类型的

3、函数模板(Function Templates)

  • 3.1 定义模板需要注意 一个 template <typename T> 只能泛化一个函数模板。
// 函数模板使用的定义,这个是泛化下面一个add,   这个 T 就是一个数学上的变量,换成任何字母都可以。
template <typename T>         
T add(T a, T b) {  
    return a + b;  
}
//
template <typename X>  
X add(X a, X b) {  
    return a + b;  
}


在这里插入图片描述

  • 3.1.1 编译器自动识别传入参数的类型是什么类型,我们可以不用指定。如果没有传入参数,又调用用到数据类型的模板,则一定需要指定。
#include <iostream>

using namespace std;

// 函数模板定义
template <typename T>
T add(T a, T b) {
    return a + b;
}

int main() {
    int result = add<int>(3, 4);        // 调用 int 类型的 add,编译器自动识别传入参数的类型是int型,不加<int>也可以。
    cout << "result = " << result << endl;

    float result2 = add(3.5f, 4.5f); // 调用 float 类型的 add,编译器自动识别传入参数的类型是float型
    cout << "result2 = " << result2 << endl;


    // 还可以调用其他类型的 add,比如 double
    double result3 = add(3.5, 4.5);
    cout << "result3 = " << result3 << endl;

    return 0;
}
  • 3.1.2 如果没有传入参数,又调用又类型的模板,则需要指定。调用时候,使用add< int > 这种格式
#include <iostream>

using namespace std;

// 函数模板定义
template<typename T>
T add(T a, T b) {
    return a + b;
}


// 函数模板定义
template<typename T>
T demo() {
    cout << "No parameter";
}

int main() {

//    demo();
    demo<int>();//如果这个模板没有用到参数,我们需要指定这个T是什么类型。
    return 0;
}
  • 3.2、函数模板特性之重载
    • 和普通函数一样,函数模板也可以重载
#include <iostream>

using namespace std;

template<typename T>
T add(T t1, T t2) {
    T ret;
    ret = t1 + t2;
    return ret;
}

template<typename T>
T add(T t1, T t2, T t3) {
    T ret;
    ret = t1 + t2 + t3;
    return ret;
}

int main() {

    int result1 = add(1, 2);
    int result2 = add(1, 2, 3);

    cout << "result1 = " << result1 << endl;
    cout << "result2 = " << result2 << endl;

    return 0;
}

  • 3.3 多个参数的函数模板

当需要使用多个参数的函数模板时,可以在模板定义中添加额外的类型参数。这样,实例化函数模板时可以提供多个类型参数,并根据需要进行实例化。下面的示例,就用到了两个类型参数 T 和U

#include <iostream>

template <typename T, typename U>
void PrintPair(T first, U second) {
    std::cout << "Pair: " << first << ", " << second << std::endl;

}

int main() {
    PrintPair(10, 3.14);                    // 实例化函数模板为 PrintPair<int, double>(10, 3.14)



    PrintPair<char*,std::string>("Hello", std::string("World"));   // 实例化函数模板为 PrintPair<const char*, std::string>("Hello", std::string("World"))
    return 0;
}

  • 4、 类模板(Class Template)
    • 类模板和函数区别不大
    • 类模板(Class Template)是 C++ 中用于创建通用类的特性。它允许定义一个模板,其中的类型参数可以在使用时被实例化为具体的类型,从而创建多个具有相似结构和行为的类。

类模板的定义形式如下:

template <typename T>
class ClassName { 
    // 类成员和成员函数定义
};

11、const的使用

1、 在C++中,const关键字用于声明一个变量或对象为常量,即该变量或对象在初始化后不能被修改。const关键字的主要用途包括:

  1. 保护数据不被修改:确保某些数据在程序的运行期间不会被意外修改。
  2. 提高代码的可读性和可维护性:通过明确哪些数据是只读的,使代码更易于理解和维护。
  3. 优化:编译器可以利用const信息来生成更高效的代码,因为一些优化可以在编译时确定。

2、const可以修饰 普通变量,函数传入的参数变量 ,函数返回的变量

  • 2.1 修饰普通变量:

通过在变量声明前加上const关键字,可以将变量声明为常量。如果没有在定义的时候,就赋值,之后也赋不了值了。

const int x = 5;  // 声明一个常量x,其值为5,不可修改

防止意外修改:将变量声明为 const 可以防止意外的修改,提高代码的可靠性和可维护性。

  • 2.2 修饰函数的入参:
void func(const int x) {
    // x 在函数内是只读参数,不可修改 
}

这样的格式,函数参数为只读:将函数参数声明为const表示函数内部不会修改该参数的值。
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/b02bb0a7fab14fd7934ea94854d1ed30.png =700x

  • 2.3 修饰函数返回值:

返回值为只读:将函数返回值声明为const表示返回值为只读,不能被修改。这个返回值,指的是 已经被返回的值,即调用这个函数后,返回的那个值。

const int getValue() {
    return 42;
}

#include <iostream>

using namespace std;

class A {
private:
    int num;
public:
    A() : num(2) {}

    void set_num() {
        this->num = 10;
    }

    void get_num() const {
        printf("%d\\n", num);
    }
};

class B {
public:
    //这个 返回的是一个 const 类型的  class A 类型的指针。因为这个 const是在 * 的左边,所以这个指针的指向的内容不能修改。
    const class A *get() {
        class A *p = new A();
        return p;
    }
};

int main() {
    B b;
    b.get()->get_num();
//    b.get()->set_num();  // 编译失败,因为返回的对象是const修饰,所以不允许修改成员,需要注释掉
    return 0;
}
  • 2.4 修饰类成员

    常量成员变量:在类中使用 const 声明的成员变量是类的常量成员,一旦初始化后就不能修改
    常量成员函数:在类中使用 const 修饰的成员函数表示该函数不会修改类的成员变量

class MyClass {
private:
    const int value;  // 声明一个常量成员变量

public:
    int getValue() const {
        return value;  // 常量成员函数,不能修改非静态成员变量
    }
};

  • 2.5 修饰指针

const在*右侧:表示指针本身是一个常量,即指针指向的内存地址不能修改。

int x = 5;
int* const ptr = &x;  // ptr 是一个指向整型变量的常量指针
*ptr = 10;  // 可以修改指针指向的值
ptr++;  // 错误,指针本身是一个常量,不能修改指向的地址

const在 *左边:表示指针指向的值是一个常量,即指针所指向的内存地址的值不能修改。

int x = 5;
const int* ptr = &x;  // ptr 是一个指向常量整型的指针
*ptr = 10;  // 错误,不能修改指针指向的值
ptr++;  // 可以修改指针指向的地址

const在*左右都有:表示指针本身是常量,且指针指向的对象也是常量。

const int x = 5;
const int* const ptr = &x;  // ptr 是一个指向常量整型的常量指针
*ptr = 10;  // 错误,不能通过指针修改 x 的值
ptr++;  // 错误,指针本身和指向的对象都是常量

  1. 总结
    使用 const 的好处:
  • 提高代码的可读性:通过明确标识常量和只读的对象,使代码更易于理解和维护。
  • 防止意外修改:将对象声明为 const 可以防止在代码中无意中修改其值,提高代码的稳定性和可靠性。
  • 编译器优化:编译器可以使用 const 信息进行优化,提高代码的执行效率。
    需要注意的是,const 对象必须在声明时进行初始化,并且其值在编译时就确定了,不能在运行时修改。

在编写代码时,合理使用 const 可以提高代码的可读性、可靠性和性能。通过将对象声明为 const,可以明确指示其只读特性,并减少意外的修改,从而提高代码的质量和可维护性。

12、引用

1、引用是什么

  • 引用一个变量,就是给这个变量起一个新名字,并且通过这个新名字,可以直接操作这个变量本身,是操作原始对象而不是复制对象。这个听起来很像指针。

2、 引用的定义

引用是一个已存在对象的别名,它用于在代码中引用和操作该对象。引用的定义使用 & 符号,并在变量类型前加上 &。

int num = 10;
int& ref = num;  // 引用变量 ref 是变量 num 的别名

在上述示例中,我们声明了一个整型变量 num,然后通过 int& ref = num 将 ref 声明为 num 的引用。此时,ref 就成为了 num 的别名,对 ref 的操作实际上是对 num 的操作

3、 引用的特性

  • 引用没有独立的存储空间,它只是变量的别名,与原始变量共享同一块内存。
  • 对引用的操作等效于对原始对象的操作,对引用的修改会直接反映到原始对象上。
  • 引用可以用于函数参数传递和返回值,允许直接操作原始对象而不是复制对象。
  • 引用可以提高代码的可读性,使代码更加直观和简洁。

4、引用(Reference) 与指针(Pointer)的比较

  • 引用和指针是 C++ 中的两种不同的概念。它们都提供了对对象的间接访问方式,但在语法和语义上有一些区别。

  • 引用没有独立的存储空间,它只是变量的别名,但是指针有。

  • 引用在声明时必须初始化,并且不能引用空值(NULL),而指针可以在声明后再进行初始化,并且可以指向空值。

  • 引用一旦初始化后不可改变,总是引用同一个对象,而指针可以改变所指向的对象。

  • 引用可引用指针(Reference to pointer is ok),指针不能指向一个引用(No refereence to references),引用也不能引用另一个引用(No references to reference)。

  • 引用不需要进行内存分配和释放,而指针需要手动进行内存管理。

  • 其实引用的性质,就是一个 const 类型的指针

5、引用(Reference) 的用法

  • 1、引用作为函数参数

引用常用于函数参数传递,允许在函数中直接操作原始对象,而不是复制对象的副本。通过使用引用参数,可以避免对象复制的开销,并使函数对原始对象的修改能够在函数外部可见。

使用的方式就是 定义函数的时候,形参是 int& 这种类型,表示接受一个引用。然后调用这个函数的时候,直接传入变量本身就行。

#include <iostream>

using namespace std;

int num; 
int *p;
int & xx=num;//引用,给变量num起一个名字
void modifyValue(int& value) {  
    value = 10;  
}

int main() {  
    int num = 5;  //直接传入这个变量就可以了
    modifyValue(num);  // 通过引用修改原始对象  
    // 现在 num 的值为 10  
    return 0; 
}

上述里,我们定义了一个函数 modifyValue,它接受一个整型引用参数 value。通过引用参数,我们就可以直接修改原始对象 num 的值。

  • 2、引用作为函数返回值

引用还可以作为函数的返回值,即 允许函数返回对其他变量的引用。这样可以方便地在表达式中使用函数返回的引用,并直接修改原始对象。

#include <iostream>
using namespace  std;


int& getLarger(int& a, int& b) {
    return (a > b) ? a : b;
}

int main() {
    int num1 = 5;
    int num2 = 10;
    int & largerNum = getLarger(num1, num2);  // 获取较大值的引用.
    int & test=largerNum;  //它引用了largerNum所引用的同一个对象。这里的关键是理解,test和largerNum都是指向同一个对象的别名,而不是指向另一个引用的引用。
    //int&& rr = a; // 这种才是引用的引用,但是时错误的,不能声明引用的引用
    test=22; // 直接修改原始对象
    cout<<"the numb of test is "<<test<<"\n";
    largerNum = 15;  // 直接修改原始对象
    cout<<"the numb of largerNum is "<<largerNum;

    // 现在 num2 的值为 15
    return 0;
}

在上述示例中,我们定义了一个函数 getLarger,它接受两个整型引用参数 a 和 b。函数通过比较两个参数的值,返回较大值的引用。我们将返回的引用赋给 largerNum 变量,并直接修改原始对象 num2 的值。

  • 引用作为函数参数和返回值的注意事项
    引用作为函数参数传递时,函数可以修改原始对象的值,因此需要小心操作引用,确保不会意外修改原始对象。
    引用作为函数返回值时,确保返回的引用所引用的对象在函数调用结束后仍然有效。避免返回局部对象的引用,因为局部对象在函数返回后会被销毁。

  • 3、引用和常量
    引用可以与常量一起使用,从而创建常量引用。常量引用在函数参数传递中很有用,以避免对原始对象进行修改。

void printValue(const int& value) {
    // 只读访问 value,不会修改原始对象
}

int main() {
    int num = 10;
    printValue(num);  // 通过常量引用传递参数
    return 0;
}

在上述示例中,我们定义了一个函数 printValue,它接受一个整型常量引用参数 value。通过使用常量引用,我们确保函数内部只能读取参数的值,而不会修改原始对象 num。

  • 4、引用和数组

引用可以与数组一起使用,以引用数组的元素或作为数组的别名。上面有说,引用看起来,就是一个const 指针。数组呢,数组名其实也类似一个const指针。

void printArray(const int (&arr)[5]) {
    for (int i = 0; i < 5; i++) {
        cout << arr[i] << " ";
    }
    cout << endl;
}

int main() {
    int nums[] = {1, 2, 3, 4, 5};
    printArray(nums);  // 通过引用传递数组
    return 0;
}

在上述示例中,我们定义了一个函数 printArray,它接受一个整型数组的引用参数。通过使用数组引用参数,我们可以在函数内部访问数组的元素,并在函数外部调用时避免了数组的复制。

  • 5、引用和结构体/类

引用可以与结构体或类一起使用,以引用结构体或类的对象。

struct Point {
    int x;
    int y;
};

void printPoint(const Point& p) {
    cout << "x: " << p.x << ", y: " << p.y << endl;
}

int main() {
    Point pt = {10, 20};
    printPoint(pt);  // 通过引用传递结构体对象
    return 0;
}

在上述示例中,我们定义了一个结构体 Point,它包含两个整型成员变量 x 和 y。然后,我们定义了一个函数 printPoint,它接受一个 Point 结构体对象的引用参数。通过引用参数,我们可以在函数内部访问结构体对象的成员。

13、 C++的容器是什么
  • 保存一组给定类型对象的类型叫容器, 在C++中,容器是用于存储和管理数据的对象。

  • 容器提供了一种将多个元素组织在一起的方式,并提供了一系列操作来方便地访问、插入、删除和修改数据。C++标准库提供了许多不同类型的容器,每种容器都有其特定的功能和用途。

  • 通俗的来解释C++中的容器,可以把容器想象成是我们日常生活中用来装东西的各种“盒子”或“袋子”。在C++编程里,容器就是那些用来存储数据的“盒子”或“袋子”,袋子里装的都是各式各样的工具。

  • 想象一下,你有一个vector(向量)容器,它就像一个可以动态调整大小的数组。你可以把整数、浮点数、字符串,甚至是自定义的对象放进这个“数组盒子”里,然后按照索引来访问它们,就像你在现实生活中从一个数组里拿东西一样。

  • 再来看list(列表)容器,它就像是一个双向链表,你可以在任何位置插入或删除元素,而不需要像操作数组那样移动大量的数据。这个“链表袋子”非常灵活,适合那些需要频繁进行插入和删除操作的场景。

  • 还有set(集合)和map(映射)容器,它们更像是专门用来存储唯一元素和键值对的“特殊盒子”。set保证了里面的元素都是独一无二的,而map则允许你通过一个键来快速查找对应的值,就像你在现实生活中使用字典查找单词一样。

  • 这些容器不仅提供了存储数据的功能,还封装了许多常用的操作,比如遍历、排序、查找等。这样,你就可以专注于实现业务逻辑,而不需要自己从头开始编写这些底层的数据管理代码了。

13、 C++的lambda表达式
  • python当作,一个函数可以嵌套另一个函数,单数在 C++中,函数内部是不能嵌套一个新的函数。
  • 然而,C++11及更高版本引入了lambda表达式,这提供了一种在函数内部定义“匿名函数”的方式,这些匿名函数可以捕获外部变量的值并在函数内部使用。Lambda表达式可以看作是函数对象的一种简便写法。例如
#include <iostream>

void outerFunction() {
    auto innerFunction = []() {
        std::cout << "This is an inner function using lambda." << std::endl;
    };
    innerFunction(); // 调用lambda表达式
}

int main() {
    outerFunction();
    return 0;
}
  • lanbda语法格式如下:
 //语法
[捕获列表](参数列表)->返回值类型{函数体}
     
  • 参数列表:在lambda表达式中,参数列表是你希望接受的参数,所以参数列表是可选的,因为不用入参也可以。但如果希望lambda接受参数,则必须提供它们。
[](int a ,int b)->int{return a + b ;} ; //一个简单的加法
  • 返回值类型:同样地,返回值类型也是可选的。编译器会根据lambda体的内容来推导返回类型。但是,如果您希望指定一个与推导出的类型不同的返回类型,可以使用->语法来显式指定。
[](int a ,int b){return a + b ;} ; 
//或
[](int a ,int b)->{return a + b ;} ; 
  • 捕获列表:捕获列表是中括号[]内的内容,用于指定哪些外部变量(如果有的话)可以在lambda体内被访问。捕获列表是必需的,即使它不包含任何变量,也必须提供一个空的中括号[]。捕获列表可以包括按值捕获(使用=或变量名)和按引用捕获(使用&或&&以及变量名)。

    • [a] 表示值传递方式捕获变量 a
    • [=] 表示值传递方式捕获所有父作用域的变量(包括this)
    • [&a] 表示引用方式传递捕获变量a
    • [&] 表示引用传递方式捕获所有父作用域的变量(包括this)
    • [this] 表示值传递方式捕获当前的this指针
    • [=,&a,&b] 引用方式捕获 a 和 b , 值传递方式捕获其他所有变量 (这是组合写法)
  • 函数体:lambda表达式的函数体通常是花括号{}内的代码块,它包含了lambda要执行的逻辑。如果函数体只包含一条语句,并且我们希望该语句的结果作为lambda的返回值,那么可以省略花括号和return关键字(这被称为lambda的“简洁形式”)。但是,如果函数体包含多条语句或需要显式地返回某个值,则必须使用花括号。

#include<iostream>

using namespace std;

int main(){
    
    int a = 3 ;
    int b = 5;

    
    //值传递方式捕获 a 和 b , 在lanmbda 表示内部无法修改a和b的值
    auto f1 = [a,b]{return a + b;}; 
    cout << f1()  << endl; //打印 8


    //引用方式捕获 a 和 b  , 可以在内部修改a 和 b的值
    auto f2 = [&a,&b]{ 
        a = 30; //这里修改会导致外部的a 也跟着修改。
        return a + b;
    };
    
    cout << f2()  << endl; //这里打印35
    cout << "a= "<< a << endl; //再打印一次,a 变成30了
    
    return 0 ;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值