C++面向对象

对象与类

什么是方法,对象与类

不解释

类可以嵌套.另外struct也可以当成类来实现,它是变量默认全public,而class默认全private.

类作用域

在声明里如果想初始化一个常量可以用枚举,或者static const,但是只有const不行

C++11后可以直接使用const了.

访问控制

private:只能类的内部(比如成员函数)可以访问,进而可以通过调用public的成员函数能间接访问私有变量.

public:外部可以直接访问.

protected:继承相关.

成员函数

定义

顾名思义.

定义成员函数的时候可以用::来表示函数所属的类

void Stock::updata(double price)

内联

如果愿意,可以在类声明后定义成员函数,然后用inline来表示内联.

由于内联函数的特殊,要求每个使用它们的文件中都要对其定义,所以建议直接把内联定义凡在定义类的头文件中,尤其是小巧的函数.

另外,在C++17后内联可以用来定义类内静态数据成员

常量成员函数

const Stock & topval(const Stock &s) const;最后一个const就代表常量成员函数,代表着一种不变性.

意思就是,带有这个const的成员函数不能改变但是可以访问调用对象的非静态数据成员(除非它带上mutable关键字),可以修改static成员变量;

并且它只能调用常量成员函数,mutable此处不生效.

常量对象只能调用常量函数(这就是为什么大多数模板要实现常量和非常量两个函数),对非常量对象没有限制.

构造与析构

构造函数有以下几种,而且有些时候会根据有无存在相应构造函数来实现相关功能,需要严格按照其定义实现:

  • 普通构造函数

    与类名同名,没有返回值类型(包括void也不能有)。可以有参数,也可以没有参数,用于为对象的成员变量赋初值。

    class Rectangle {
    private:
        int width;
        int height;
    public:
        // 带参数的普通构造函数
        Rectangle(int w, int h) : width(w), height(h) {} 
        // 不带参数的普通构造函数(默认构造函数的一种形式)
        Rectangle() : width(0), height(0) {} 
        int getArea() const {
            return width * height;
        }
    };
    
  • 默认构造函数

    可以是用户自定义的没有参数的构造函数,也可以是编译器自动生成的(前提是类中没有定义任何构造函数,且类满足一定条件,比如所有非静态成员都有默认初始化方式 )。默认构造函数用于在创建对象时,如果用户没有提供初始化参数,就按照默认的方式对对象进行初始化。

    class Point {
    private:
        int x;
        int y;
    public:
        // 用户自定义的默认构造函数
        Point() : x(0), y(0) {} 
    };
    //下面是没有生成构造函数的例子
    class Circle {
    private:
        double radius;
    };
    // 编译器自动生成的默认构造函数大致等价于:
    // Circle::Circle() {
    //     // 对基本类型成员,默认值不确定,对类类型成员,会调用其默认构造函数
    // } 
    
  • 拷贝构造函数

    函数形参是本类对象的引用(通常是const引用,以避免不必要的拷贝),用于使用一个已存在的同类型对象来初始化新创建的对象。如果用户没有定义拷贝构造函数,编译器也会自动生成一个默认的拷贝构造函数,它会逐个成员进行浅拷贝。

    class Student {
    private:
        std::string name;
        int age;
    public:
        Student(const std::string& n, int a) : name(n), age(a) {}
        // 自定义拷贝构造函数
        Student(const Student& other) : name(other.name), age(other.age) {} 
        //编译器会生成的类型
    };
    
  • 移动构造函数

    C++11 引入,函数形参是本类对象的右值引用(形如ClassName(ClassName&& other) ),用于在对象移动时高效地转移资源所有权,而不是像拷贝构造函数那样进行数据拷贝,从而提高性能,避免不必要的资源复制。如果用户没有定义移动构造函数,在满足类中没有用户自定义的移动构造函数,拷贝构造函数,拷贝赋值运算符,析构函数且非静态变量和基类都符合移动语义时,编译器会自动生成一个默认的移动构造函数。

    原因分别为:

    • 如果类中已经定义了一个移动构造函数(包括显式定义和删除的移动构造函数),编译器就不会再生成默认的移动构造函数。因为编译器认为用户已经对对象移动时的资源处理有了明确的规划
    • 若定义了拷贝构造函数,编译器会认为用户对对象的复制操作有特定的要求,这种要求可能和移动操作存在关联,或者用户想对对象的复制和移动行为做统一控制,此时编译器不会生成默认移动构造函数。
    • 拷贝赋值运算符用于将一个对象的值赋给另一个对象。当定义了拷贝赋值运算符,编译器会认为用户对对象之间的赋值操作有特殊需求,和移动操作可能存在冲突,就不会生成默认移动构造函数。
    • 析构函数用于在对象生命周期结束时释放资源。如果定义了析构函数,意味着对象内部可能涉及到一些复杂的资源管理,编译器无法简单地确定移动操作的安全性和正确性,也就不会生成默认移动构造函数。
    • 类的每个非静态成员变量类型都必须支持移动构造或者拷贝构造。如果存在不支持移动构造且也没有合适拷贝构造函数的成员变量类型,编译器就无法生成默认移动构造函数。例如,类中有一个成员变量是自定义的不支持移动构造的类类型,且没有定义合适的拷贝构造函数,编译器就不会生成默认移动构造函数。
    • 如果类继承自其他类,基类也需要满足上述条件,即基类要么有默认移动构造函数,要么有用户自定义的合适的移动构造函数、拷贝构造函数等。
  • 委托构造函数

    一个构造函数通过this关键字调用同一个类中的其他构造函数来完成对象的部分或全部初始化工作。这种方式可以避免在多个构造函数中重复编写相同的初始化代码。

    class Complex {
    private:
        double real;
        double imag;
    public:
        Complex(double r) : real(r), imag(0) {}
        // 委托构造函数,调用上面的单参数构造函数
        Complex(double r, double i) : Complex(r) { 
            imag = i;
        }
    };
    //Complex(double r, double i)构造函数委托Complex(double r)构造函数先初始化real成员,然后再初始化imag成员。
    
  • 转换构造函数

    是只有一个参数(或者除了第一个参数外,其他参数都有默认值)的构造函数,它可以将参数类型隐式转换为本类对象。这种构造函数常用于实现类型转换。

    class Integer {
    private:
        int value;
    public:
        // 转换构造函数,将int转换为Integer对象
        Integer(int num) : value(num) {} 
        int getValue() const {
            return value;
        }
    };
    
    Integer num = 10;  // 隐式调用转换构造函数,将int 10转换为Integer对象
    

析构是销毁对象时候要用的,析构的名字是~对象名字().有动态变量时有着重注意实现.

显式禁用默认函数

在传统 C++ 中,如果程序员没有提供,编译器会默认为对象生成默认构造函数、 复制构造、赋值算符以及析构函数。 另外,C++ 也为所有类定义了诸如 new delete 这样的运算符。 当程序员有需要时,可以重载这部分函数。

这就引发了一些需求:无法精确控制默认函数的生成行为。 例如禁止类的拷贝时,必须将复制构造函数与赋值算符声明为 private。 尝试使用这些未定义的函数将导致编译或链接错误,则是一种非常不优雅的方式。

并且,编译器产生的默认构造函数与用户定义的构造函数无法同时存在。 若用户定义了任何构造函数,编译器将不再生成默认构造函数, 但有时候我们却希望同时拥有这两种构造函数,这就造成了尴尬。

C++11 提供了上述需求的解决方案,允许显式的声明采用或拒绝编译器自带的函数。 例如:

class Magic {
    public:
    Magic() = default; // 显式声明使用编译器生成的构造
    Magic& operator=(const Magic&) = delete; // 显式声明拒绝编译器生成构造
    Magic(int magic_number);
} 

类的初始化方式

默认初始化

在定义对象时不提供任何初始化值,对于内置类型成员变量,其值是未定义的(对于全局或静态存储期的内置类型变量,会初始化为 0 );对于类类型成员变量,会调用其默认构造函数进行初始化。如果类没有默认构造函数,就无法进行默认初始化。

class Point {
public:
    int x;
    int y;
    Point() = default;  // 使用编译器生成的默认构造函数
};

int main() {
    Point p;  // 默认初始化,x和y值不确定(这里使用默认构造函数)
    return 0;
}

值初始化

对于内置类型数组,会初始化为0;对于类类型对象,会调用默认构造函数,如果类没有默认构造函数,会使用占位符初始化(对于基本类型初始化为0,对于类类型会尝试调用无参构造函数,如果没有则报错)。常见的形式有:使用圆括号()对单个对象初始化、使用花括号{}进行列表初始化(空列表时) 。

class Rectangle {
public:
    int width;
    int height;
    Rectangle(int w = 0, int h = 0) : width(w), height(h) {}
};

int main() {
    Rectangle r1();  // 值初始化,等价于Rectangle r1(0, 0);
    Rectangle r2{};  // 值初始化,width和height被初始化为0
    int arr[3]{};  // 值初始化,数组元素都为0
    return 0;
}

直接初始化

使用圆括号(),根据构造函数的参数列表,提供相应的初始化值,直接调用匹配的构造函数来初始化对象。

class Circle {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    double getArea() const {
        return 3.14 * radius * radius;
    }
};

int main() {
    Circle c(5.0);  // 直接初始化,调用Circle(double r)构造函数
    return 0;
}

拷贝初始化

使用赋值号=,先创建一个临时对象,然后通过拷贝构造函数将临时对象的值拷贝给新对象(C++11 后引入了移动语义,在满足条件时会使用移动构造函数来提高效率 )。

class Student {
private:
    std::string name;
    int age;
public:
    Student(const std::string& n, int a) : name(n), age(a) {}
    Student(const Student& other) : name(other.name), age(other.age) {}
};

int main() {
    Student s1("Alice", 20);
    Student s2 = s1;  // 拷贝初始化,调用拷贝构造函数
    return 0;
}

列表初始化

使用花括号{},可以对类对象进行初始化,它会优先匹配接受std::initializer_list参数的构造函数(如果有的话),如果没有则按照成员变量声明顺序依次进行初始化,对于没有默认构造函数的成员变量,必须在初始化列表中提供初始值。

class Person {
private:
    std::string name;
    int age;
public:
    Person(const std::initializer_list<int>& list) {
        if (list.size() == 2) {
            age = *list.begin();
            // 这里假设简单转换,实际中可能需要更复杂逻辑
            name = std::to_string(*(list.begin() + 1)); 
        }
    }
    Person(const std::string& n, int a) : name(n), age(a) {}
};

int main() {
    Person p1{"Bob", 25};  // 列表初始化,调用Person(const std::string&, int)构造函数
    Person p2{28, 3};  // 列表初始化,调用Person(const std::initializer_list<int>&)构造函数
    return 0;
}

委托构造初始化

一个构造函数通过this关键字调用同一个类中的其他构造函数来完成对象的部分或全部初始化工作,减少代码重复。

class Time {
private:
    int hour;
    int minute;
    int second;
public:
    Time(int h) : hour(h), minute(0), second(0) {}
    Time(int h, int m) : Time(h) {  // 委托Time(int h)构造函数
        minute = m;
    }
    Time(int h, int m, int s) : Time(h, m) {  // 委托Time(int h, int m)构造函数
        second = s;
    }
};

this指针

使用被称为this的特殊指针。该指针指向用来调用成员函数的对象(this被作为隐藏参数传递给方法)。如果需要引用整个调用对象,可以用*this,在函数括号后面使用constthis限定为const,这样就不能使用this修改对象函数.

如果没有歧义的话,this是可以省略的,直接调用成员变量.

友元函数

friend

类的继承

主要学习C++之继承详解(万字讲解)_c++继承-CSDN博客

定义

class father{...};

class son:public father{
    ...
};
//father为父类,public代表继承关系,可以继承多个类.

继承关系

类成员 / 继承方式 public 继承 protected 继承 private 继承
基类的 public 成员 派生类的 public 成员 派生类的 protected 成员 派生类的 private 成员
基类的 protected 成员 派生类的 protected 成员 派生类的 protected 成员 派生类的 private 成员
基类的 private 成员 在派生类中不可见 在派生类中不可见 在派生类中不可见

在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强.

如果想要某个类不被继承,可以使用final关键字.

继承中的作用域

在继承体系中基类和派生类都有独立的作用域。

子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显式访问)

需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。

注意在实际中在继承体系里面最好不要定义同名的成员。

切片

派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。

当然,基类对象不能赋值给派生类对象。除非基类的指针或者引用通过强制类型转换赋值给派生类的指针或者引用。但是此处基类的指针必须是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(RunTime Type Information)的dynamic_cast 来进行识别后进行安全转换。

继承后的同名函数会根据指针的类型决定调用哪个,特别是运用引用的情况下.

派生类的默认成员函数

子类会默认调用父类的相应成员函数,无需操心

友元

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员

静态成员

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例。

虚拟继承

如果有这种情况:

classDiagram direction RL class Base { +Base() #int base_data +void base_func() } class Derived1 { +Derived1() #int d1_data +void d1_func() } Derived1 --|> Base : 继承 class Derived2 { +Derived2() #int d2_data +void d2_func() } Derived2 --|> Base : 继承 class FinalDerived { +FinalDerived() #int d1_data +void final_func() #int d2_data } FinalDerived --|> Derived1 FinalDerived --|> Derived2 note for FinalDerived "出现问题, 最后的类对于相同的数据要实现两份" note for Base "基类:被多个派生类继承"

可以看到d1_data和d2_data都被实现了.当然我们可以显式地访问相应数据,但是我们肯定不乐意这样做,所以有了virtual,当derived1和2都虚拟继承base时,finalderived直接继承两者,两个类型中一样的部分的指向改为同一个地址.

最好不要出现菱形继承关系,这样关系也很乱.

类的多态和RTTI

掌握虚函数、纯虚函数与抽象类:C++多态基石详解-CSDN博客

RTTI(runtime type identication)运行时多态

override final 最好直接用.

如果存在虚函数,无论实例化几个类,都会多出空间存放虚表的指针,但是虚表是同一张.

classDiagram direction LR class 基类 { +虚表指针 +基类成员变量: int +虚析构函数() +虚函数1() +虚函数2() } class 派生类 { +虚表指针 +基类成员变量: int +派生类成员变量: int +虚析构函数() +重写虚函数1() +新增虚函数3() } class 基类虚表 { 虚表 +[0]: &基类::~基类() +[1]: &基类::虚函数1() +[2]: &基类::虚函数2() } class 派生类虚表 { 虚表 +[0]: &派生类::~派生类() +[1]: &派生类::重写虚函数1() vfptr指向函数 +[2]: &基类::虚函数2() // 继承未修改 +[3]: &派生类::新增虚函数3() // 扩展 } 基类 --> 基类虚表 : 虚表指针指向 vtable 派生类 --> 派生类虚表 : 虚表指针指向 vtable
posted @ 2025-08-30 13:48  T0fV404  阅读(4)  评论(0)    收藏  举报