C++复习笔记-面向对象

C++复习-面向对象

这里复习了一些面向对象中的一些内容,是自己在复习的过程中所记录的笔记,希望可以帮到到你。
博客原文链接

面向对象编程和编写对象编程需要不断写代码累计相关的经验。

简单描述,面向过程就是想先干嘛,在干嘛,想动作。面向对象想的是这个东西是啥,能干啥,和别的东西啥关系。

哈哈哈,这么说也比较抽象,大家将就理解理解。

类和结构体

C中只有结构体,C++兼容结构体的用法,并且创造了类class。一个类可以实例化出多个对象。

结构体只有数据,而类不但有数据,还有这个类的方法,类的函数被成为成员函数。

类的函数方法可以直接使用类的属性,而不用再传入。

C中定义结构体:

typedef struct ListNodeC
{
    struct ListNodeC* next;
    int val;
}LNode;

C++中定义结构体:

struct ListNodeCPP
{
    ListNodeCPP* next;
    int val;
}

限定符号

C++中有种访问限定符号public,private

C++种默认的是private的限制,及私有限制。而public是公有限制。一个共有限制符号之后全部都是共有,直到出现下一个限定符号。

而struct是默认为公有限制的。

class sin{
public:
    void add();
private:
    int a;
    bool c;
}

一般在定义类的时候,需要将方法设为公有,将成员属性设置为私有。改变成员函数需要使用类的方法才可以。

面向对象编程有三大特性:封装继承多态

类的作用域,是C++四大作用域其中的一个。一般在C++类中声明函数,之后再定义的时候,需要用注明是哪一个类的作用域中的。

类在声明的时候并没有开辟空间,只是进行了声明。对象进行实例化只会才会开辟空间。

结构体内存对齐规则

第一个成员在于结构体偏移量为0的地址处。

其他成员变量要对其到某个数字(对齐数)的整数倍的地址处。

注意:对齐数=编译器默认的一个对齐数与该成员大小的较小值。

VS中默认的对齐数为8。

如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

可以使用 #pragma pack(n) 修改对齐方式。例如:

#pragma pack(1)  // 取消对齐,所有成员按实际大小存储
struct Packed {
    char a;
    int b;
    short c;
};
#pragma pack()  // 还原默认对齐

关于结构体对齐的详细内容可以参考:https://cloud.tencent.com/developer/article/1703257

类的实例化

下面给一段代码:

#include <iostream>
using namespace std;

class A {
public:
    void Print() {
        cout << "123" << endl;
    }

private:
    int _a;
};

int main() {
    A *p = nullptr;  // p 是一个空指针
    p->Print();      // 这里为什么不会崩溃?
    return 0;
}

在 C++ 中,如果 p 是一个 空指针(nullptr),调用 p->Print() 之所以不会导致 访问空指针 导致崩溃,是因为 Print() 没有访问类的成员变量。

C++ 中 非虚函数 的调用,本质上是 静态绑定(static binding),等价于:A::Print();

因为 Print() 没有用到任何成员变量,所以它只是一个普通的全局函数(本质上和 A 的实例无关),调用它不会访问 nullptr,不会触发崩溃。

但如果 Print() 访问了成员变量,就会崩溃:

class A {
public:
    void Print() {
        cout << _a << endl;  // 这里访问了成员变量
    }

private:
    int _a;
};

int main() {
    A *p = nullptr;
    p->Print();  // 这里会崩溃!因为访问了 _a
    return 0;
}

p->Print() 其实等价于 Print§,即 this = p 传入 Print() 函数。

但 p == nullptr,所以 this 也是 nullptr,一旦访问 this->_a,就会访问 非法内存地址,导致 崩溃(Segmentation Fault)。

如果是虚函数呢?

如果 Print() 是一个 虚函数(virtual function),情况就不同了:

class A {
public:
    virtual void Print() {
        cout << "123" << endl;
    }
};

int main() {
    A *p = nullptr;
    p->Print();  // 这里会崩溃!
    return 0;
}

为什么会崩溃?

  • 由于 Print() 是 虚函数,调用 p->Print() 时,编译器会生成 虚函数表(vtable) 的查找代码。
  • p 是 nullptr,p->vtable 也是 nullptr,访问 vtable 就会导致崩溃。

this指针存在哪里呢?

this指针并没有存储在对象中,this存储在栈上,因为this是一个形参。

形参和局部变量都是存储在栈上面。

有些地方会把this指针放在ecx寄存器中,因为this指针需要频繁的访问,所以直接放在寄存器中使用。

六个默认成员函数

默认成员函数就是⽤⼾没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数。

一个类,我们不写的情况下,编译器会自动生成六个默认的成员函数。

构造函数

C语言中需要对类进行手动初始化,在没有初始化在使用的时候会引发一些问题。

类的空间局部对象创建栈帧的时候创建的,而

C++有构造函数,是对类进行初始化。函数名和类名相同。没有返回值,不需要写返回值。在对象实例化的时候自动调用。

构造函数可以重载。

析构函数

析构函数不是销毁对象,是完成对象中资源的清理工作。

没有返回值,没有参数,在对象生命周期结束的时候自动调用。

C++将数据分为内置类型和自定义类型。像int、double、char这些是内置类型,而类、结构体等是自定义类型。

编译器自动生成的构造函数,对于内置类型的成员变量不做处理,对于自定义类型的成员变量才会调用它的午餐构造。

有的编译器会将内置类型也进行初始化处理,C++的标准并没有归定,所以有些编译器会将内置类型处理。而对于自定义类型,会自动调用其的构造函数对其进行初始化。

拷贝构造函数

如果一个构造函数的一个个参数是自身类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是一个特殊的构造函数。

拷贝构造函数时一个构造函数的重载。

拷⻉构造函数的第⼀个参数必须是类类型对象的引⽤,使⽤传值⽅式编译器直接报错,因为语法逻辑上会引发⽆穷递归调⽤。拷⻉构造函数也可以多个参数,但是第⼀个参数必须是类类型对象的引
⽤,后⾯的参数必须有缺省值。

C++规定⾃定义类型对象进⾏拷⻉⾏为必须调⽤拷⻉构造,所以这⾥⾃定义类型传值传参和传值返回都会调⽤拷⻉构造完成。

若未显式定义拷⻉构造,编译器会⽣成⾃动⽣成拷⻉构造函数。⾃动⽣成的拷⻉构造对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤他的拷⻉构造。

深浅拷贝的区别是,如果是浅拷贝,那么开辟在堆种的内存会被原封不动的拷贝到另外一个类的实例中,那样你在操作一个类的实例中的此对象时,另外一个类的示例中的此对象会跟着改变。深拷贝是拷贝后另外一个类的实例会有自己新的在堆上的空间。

像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的拷⻉构造就可以完成需要的拷⻉,所以不需要我们显⽰实现拷⻉构造。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器⾃动⽣成的拷⻉构造完成的值拷⻉/浅拷⻉不符合我们的需求,所以需要我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。像MyQueue这样的类型内部主要是⾃定义类型
Stack成员,编译器⾃动⽣成的拷⻉构造会调⽤Stack的拷⻉构造,也不需要我们显⽰实现MyQueue的拷⻉构造。这⾥还有⼀个⼩技巧,如果⼀个类显⽰实现了析构并释放资源,那么他就需要显⽰写拷⻉构造,否则就不需要。

传值返回会产⽣⼀个临时对象调⽤拷⻉构造,传值引⽤返回,返回的是返回对象的别名(引⽤),没有产⽣拷⻉。

但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使⽤引⽤返回是有问题的,这时的引⽤相当于⼀个野引⽤,类似⼀个野指针⼀样。传引⽤返回可以减少拷⻉,但是⼀定要确保返回对象,在当前函数结束后还在,才能⽤引⽤返回。

赋值重载

运算符重载的定义

运算符重载是C++中的一种特性,允许开发者为类类型的对象定义特定的运算符行为。通过运算符重载,可以让类对象使用标准的运算符(如+、-、*、/等)进行操作,而这些操作的具体含义可以根据类的需求进行定制。

运算符重载的语法
class MyClass {
public:
    MyClass operator+(const MyClass& other) const;  // 重载二元运算符+
};
参数和对象数量
  • 一元运算符:只有一个参数(作为成员函数时,this指针隐式传递)。
  • 二元运算符:有两个参数(作为成员函数时,第一个参数由this指针隐式传递)。
成员函数与非成员函数

成员函数:第一个运算对象通过this指针传递,因此参数数量比运算对象数量少一个。
非成员函数:所有运算对象都作为显式参数传递。

优先级和结合性

重载后的运算符保持与原运算符相同的优先级和结合性。例如,重载+运算符后,其优先级仍然低于*和/。

限制

不可重载的运算符:.*、::、sizeof、?: 和 . 不能被重载。

不能创建新运算符:不能通过连接语法中没有的符号来创建新的运算符,例如operator@。

至少有一个类类型参数:运算符重载函数必须至少有一个类类型参数,不能仅用于内置类型对象。

特定运算符的重载

++运算符:前置++和后置++的重载方式不同。后置++需要增加一个int形参来区分:

class MyClass {
public:
    MyClass& operator++();       // 前置++
    MyClass operator++(int);     // 后置++
};

<<和>>运算符:通常重载为全局函数,以便将ostream或istream对象作为第一个参数,从而符合使用习惯:

std::ostream& operator<<(std::ostream& os, const MyClass& obj);
std::istream& operator>>(std::istream& is, MyClass& obj);
重载的合理性

并非所有运算符都适合重载。一个类应该只重载那些在语义上有意义的运算符。例如:

  • Date类可以重载-运算符(计算两个日期的差值)。
  • 但重载+运算符可能没有意义(除非有明确的语义,比如日期加天数)。

C++的运算符重载是一个强大的特性,但也需要谨慎使用。合理地重载运算符可以让代码更加直观和易于理解,但滥用可能导致代码难以维护和理解。在设计类时,应根据类的语义需求选择性地重载运算符,并确保重载后的运算符行为符合用户的直觉。

重载练习:
int operator+(int x, int y)
{
    return x - y;
}

这段代码尝试重载operator+,但它的参数都是内置类型(int),这违反了C++的规则:运算符重载函数必须至少有一个类类型参数。因此,编译器会报错:

“operator +”必须至少有一个类类型的形参

class A
{
public:
    void func()
    {
        cout << "A::func()" << endl;
    }
};

typedef void(A::*PF)(); // 成员函数指针类型

这部分代码定义了一个类A,并在类中定义了一个成员函数func。接着,使用typedef定义了一个成员函数指针类型PF,表示指向A类的成员函数,返回类型为void,没有参数。

成员函数指针的使用

PF pf = &A::func; // 取成员函数的地址
A obj;            // 定义类对象
(obj.*pf)();      // 使用对象调用成员函数指针

这部分代码是正确的:

  • PF pf = &A::func;:通过&A::func获取A类中func成员函数的地址,并将其赋值给成员函数指针pf。
  • (obj.*pf)();:使用对象obj和成员函数指针pf调用func函数。.*运算符用于对象和成员函数指针之间的调用。

赋值运算符重载

赋值运算符重载的特点

赋值运算符operator=必须定义为类的成员函数,不能作为全局函数。这是因为赋值操作需要直接访问对象的内部状态。

参数类型通常为const引用(const Date&),这样可以避免不必要的拷贝,同时保证参数不会被修改。

返回值类型为类的引用(Date&),这样可以支持链式赋值(如d1 = d2 = d3)。返回值通常是*this,即当前对象的引用。

如果没有显式定义赋值运算符,编译器会自动生成一个默认的赋值运算符。

默认实现会对内置类型成员变量进行逐字节拷贝(浅拷贝),对自定义类型成员变量会调用其赋值运算符。

如果类中包含指向动态分配资源的指针(如new分配的内存),默认的浅拷贝可能导致悬挂指针或重复释放资源。

如果类显式实现了析构函数来释放资源,通常也需要显式实现赋值运算符,以避免资源管理问题。

示例代码分析

类的定义:

class Date
{
public:
    Date(int year = 1, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }

    Date(const Date& d)
    {
        cout << "Date(const Date& d)" << endl;
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }

    Date& operator=(const Date& d)
    {
        if (this != &d)  // 防止自赋值
        {
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }
        return *this;  // 返回当前对象的引用
    }

    void Print()
    {
        cout << _year << "-" << _month << "-" << _day << endl;
    }

private:
    int _year;
    int _month;
    int _day;
};

赋值运算符的实现

  • 参数类型:const Date& d,避免拷贝并保证参数不可修改。
  • 自赋值检查:if (this != &d),防止对象给自己赋值,避免不必要的操作。
  • 返回值:返回*this,支持链式赋值。

主函数

int main()
{
    Date d1(2024, 7, 5);  // 默认构造
    Date d2(d1);          // 拷贝构造
    Date d3(2024, 7, 6);  // 默认构造

    d1 = d3;              // 调用赋值运算符
    Date d4 = d1;         // 拷贝构造
    return 0;
}

运行代码,输出可能如下:

Date(const Date& d)  // d2的拷贝构造
2024-7-6             // d1.Print(),赋值后的结果

赋值运算符与拷贝构造的区别:

赋值运算符

  • 用途:用于两个已经存在的对象之间的赋值。
  • 语法:d1 = d2;
  • 特点:需要检查自赋值。

拷贝构造函数

  • 用途:用于创建一个新对象,并用另一个对象初始化。
  • 语法:Date d4 = d1; 或 Date d4(d1);
  • 特点:不需要检查自赋值。

赋值运算符重载是C++中一个重要的特性,用于实现对象之间的赋值操作。以下是关键点总结:

  • 赋值运算符必须是成员函数,参数建议为const引用,返回值为引用。
  • 默认的赋值运算符会进行浅拷贝,可能需要显式实现深拷贝。
  • 显式实现赋值运算符时,需要检查自赋值。
  • 赋值运算符用于已存在的对象之间的赋值,而拷贝构造用于创建新对象。

初始化列表

之前我们实现构造函数时,初始化成员变量主要使⽤函数体内赋值,构造函数初始化还有⼀种⽅式,就是初始化列表,初始化列表的使⽤⽅式是以⼀个冒号开始,接着是⼀个以逗号分隔的数据成员列表,每个"成员变量"后⾯跟⼀个放在括号中的初始值或表达式。

每个成员变量在初始化列表中只能出现⼀次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地⽅。

引⽤成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进⾏初始化,否则会编译报错。

C++11⽀持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显⽰在初始化列表初始化的成员使⽤的。

尽量使⽤初始化列表初始化,因为那些你不在初始化列表初始化的成员也会⾛初始化列表,如果这个成员在声明位置给了缺省值,初始化列表会⽤这个缺省值初始化。如果你没有给缺省值,对于没有显⽰在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定。对于没有显⽰在初始化列表初始化的⾃定义类型成员会调⽤这个成员类型的默认构造函数,如果没有默认构造会编译错误。

初始化列表中按照成员变量在类中声明顺序进⾏初始化,跟成员在初始化列表出现的的先后顺序⽆关。建议声明顺序和初始化列表顺序保持⼀致。

匿名对象

匿名对象的生命周期是当前这一行,是一个即用即销毁的对象。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值