c++学习基础(一些零碎的知识点)(持续更新)

类和对象

类的创建

class 类名
{
	public:
		公有数据成员;
		公有成员函数;
	private:
		私有数据成员;
		私有成员函数;
	protected:
		保护数据成员;
		保护成员函数;
};

类其实就是前面c语言的结构体,加上了一些拓展,例如可以放入函数,有一些私有数据外部访问不到等

这三个是其中一个不同的地方 ,

公开(public)成员属性或方法在类的外部能够直接被访问和操作。

保护(protected)和私有(private)成员属性或方法则不同,它们在类的外部通常不可见,意味着不能直接访问。尽管它们都限制了外部的直接访问,但区别在于:保护成员在派生类(子类)中是可以访问的,而私有成员即使是子类也无法直接访问。(这里强调了protected和private的主要差异)

访问控制权限的适用范围始于该权限修饰符被声明的地方,一直延续到遇到下一个不同的访问控制权限修饰符时为止。如果没有后续的访问修饰符,那么当前设定的访问权限将保持有效,直到类的结尾,即“}”处。

在定义类时,默认的访问权限有所不同:对于class来说,默认所有未明确标注访问级别的成员都是私有的(private);而对于struct,考虑到与C语言结构体的兼容性,默认其成员为公开的(public),意在鼓励struct用于实现较为开放的数据结构。

构造函数

是一种特殊的成员函数,主要功能是为对象分配存储空间,以及为类成员变量赋初值

最好在初始化列表里面就把变量定义好,这样是最稳妥的

所谓初始化列表

就是冒号后面的

class CInternetBankCustomer : public CInternetUser, public CBankCustomer
{
private:
    double balance;
    double prev_balance;
    double today_profit;
    double today_interest;
    double prev_interest;
public:
    CInternetBankCustomer()
        : balance(0), prev_balance(0), today_profit(0), today_interest(0), prev_interest(0)
    {}

继承和派生

         继承机制在于将一个现有类(被称为基类或父类)的特性和功能,作为构建新类(即派生类或子类)的基础。形象地说,就如同家庭中子女继承父母的财产与特质,这里的“继承”是从子类的角度出发,强调了获取和延续;而“派生”则是从父类的视角描述,着重于传递与延续的过程。二者虽观察角度不同,实质上阐述的是同一过程:通过复用和扩展已有类的功能,创造出具有新特性的类,以支持软件的层次化设计和代码重用。

       换种说法,派生类的构造流程好比搭建一个多层建筑:首先从地基(基类)稳固开始,一层一层往上建设直至完成顶层(派生类);而析构过程则是从顶层开始拆除,直至回到地面,确保每层的清理工作在下一层开始之前完成,以此维持结构的完整性和安全性。在语法上,派生类构造函数的定义体现了这种层次关系,通过初始化列表明确指定基类及子对象的构造顺序,形式如:派生类名(参数列表) : 基类名(基类参数列表), 子对象名1(参数列表) { 派生类构造函数体; }

虚基类

C++中的虚基类机制旨在解决多重继承情况下可能出现的同名成员重复问题,确保间接继承自同一个基类的派生类只会拥有该基类的一份成员,避免了不必要的数据冗余和歧义。

  • 构造函数名必须与类名相同
  • 没有任何返回值和返回类型
  • 创建对象自动调用,不需要用户来调用,且只掉用一次
  • 类没有定义任何构造函数,编译系统会自动为这个类生成一个默认的无参构造函数
    构造函数定义(不建议系统自带的,因为有时候会跟我们想象的自动赋值不一样)
  • 
    类名::构造函数名(参数)
    {
    	函数体
    }
    

    构造函数重载:

  • 构造函数名字相同,参数个数和参数类型不一样,就是函数重载,可以按照接受的参数调用不同的函数

  • 析构函数

  • 析构函数是一种具有特殊职责的成员方法,它会在对象即将离开其生命周期,即对象被销毁时自动执行。析构函数的主要任务包括释放对象占用的资源,如动态分配的内存,以及执行必要的清理操作,确保程序的健壮性。

    析构函数的特性可以这样理解:

  • 析构函数的名字不仅要与所属的类名保持一致,还在其前面加上波浪线符号,例如,类名为Example,其析构函数则表示为~Example()
  • 这类函数设计上不接受任何参数,执行后也不返回任何值,体现了它独特的清理角色,因此不允许重载。
  • 每个类仅允许存在一个析构函数,确保资源释放的统一管理和避免潜在的冲突。
  • 如果程序员没有显式定义析构函数,编译器会自动为该类生成一个默认的析构函数。虽然默认析构函数能基本完成对象清理,但对于手动管理的资源,如动态内存,需要自定义析构函数来确保正确释放。
  • 在类名称前加上~就是析构函数了
  • 简而言之,析构函数就像是对象的“清道夫”,在对象生命终结时默默执行,确保不留资源泄露的隐患。
  • 静态成员

  • 静态成员变量,又可称作类变量,是类的一部分,不属于任何特定的类实例(对象)。这意味着,无论创建了该类的多少个对象,静态成员变量都只有一个副本,被所有这些对象共享。它在内存中只占一份空间,存储在静态存储区域,从类被加载到内存开始存在,直到程序结束时才被销毁。

    换种说法,静态数据成员就像是一个类的公共仓库,不论你用哪个对象的钥匙(对象实例)去打开这间仓库,里面存放的货物(数据)都是一样的。它不依赖于类的实例化,即使没有创建类的对象,也可以直接通过类名访问静态成员变量。这样设计使得静态成员变量非常适合用来记录与类相关的全局状态或者共享资源计数等信息。

  • class xxx
    {
    	static 数据类型 静态数据成员名;
    }

    友元

  • 友元功能允许特定的函数或类绕过常规的访问控制限制,直接访问另一个类的私有(private)和保护(protected)成员。这意味着,即使不是成员函数,通过声明为友元,一个函数或整个其他类可以获得对某个类内部私密部分的特殊访问权限。

    换种表述方式,友元函数就像是被授予了“特别通行证”的外部助手,它们虽不属于任何类的家族成员(没有this指针,表明它们不自动关联到某个特定对象),但凭借这种特殊关系,它们能够通过接收对象作为参数,自由地进出并操作那些通常对外界保密的类成员。这样一来,即使是最私密的数据成员,也能在这些被信任的友元面前展露无遗,从而实现了灵活的数据共享和操作,但这也要求开发者谨慎使用,以防破坏类的封装性和安全性。

  • 在类里面的函数前加上friend即可完成操作

  • this指针

  • 在C++中,每当一个对象调用其非静态成员函数时,编译器都会自动向该成员函数传递一个隐含的指针,这个指针被称为this指针。this指针指向正在调用该函数的对象实例,它本质上是一个指向当前对象的常量指针,让成员函数能够访问和修改调用它的对象的成员。

    换言之,this指针就像是成员函数内部的一个隐形导航员,无需显式声明,自动到位,确保成员函数知道是在哪个具体对象的上下文中执行操作。通过this,函数可以直接访问到比如this->memberVariable这样的形式来引用类的成员变量,即使函数内部有同名的局部变量也不会引起混淆。这为面向对象编程中的封装性和灵活性提供了重要支持。

  • 当然,this指针本身是const类型,不可以进行修改

  • 区别

  • 类(class)与结构体(struct)在C++中都是用于定义复合数据类型的重要工具,但它们之间存在一些根本性的差异:

  • 起源与兼容性

  • 结构体起源于C语言,目的是为了封装不同类型的数据成员。为了确保与C代码的无缝对接,C++保留了结构体的概念,同时对其进行扩展以支持面向对象编程特性。

  • 成员函数与访问控制

  • 最初,C语言的结构体仅能包含数据成员,不支持成员函数,且所有成员默认为公有(public)。C++则赋予了结构体定义成员函数的能力,并引入了访问控制(private、protected、public),使其与类功能相当,只是默认的访问权限不同。

  • 空结构体的大小

  • 在C语言中,空结构体会被优化为占据零字节的内存,而C++为了确保每个对象都有唯一的地址,规定即使是空结构体也至少分配一个字节的内存空间。

  • 默认访问权限与继承模式

  • 在C++中定义类时,若不指定成员的访问级别,默认为private;而在结构体中,默认为public。类似地,类的继承默认是private继承,意味着基类的公共和保护成员在派生类中变为私有;而结构体间的继承默认为public,保持基类成员的访问属性不变。

  • 模板使用

  • 实际上,C++中的类和结构体都可以使用模板进行泛型编程,并没有限制说只有类才能使用模板。两者在这方面是平等的。

  • 公有继承(public inheritance)

  • 如果一个类以公有方式继承自基类,基类的公有成员在派生类中依然保持公有,基类的保护成员在派生类中也是保护的。这意味着无论是派生类自身还是通过派生类的对象,都能像访问派生类自己的公有和保护成员那样去访问这些继承来的成员。但是,基类的私有成员对派生类来说仍然是隐藏的,无法直接访问。

  • 私有继承(private inheritance)

  • 当一个类以私有方式继承自基类时,基类的公有和保护成员在派生类中都被视为私有成员。这限制了对这些成员的访问仅限于派生类的内部——即只有派生类的成员函数可以直接访问这些成员,而外界(包括派生类的对象)都无法直接看到或使用它们。基类的私有成员同样保持不可访问。

  • 保护继承(protected inheritance)

  • 保护继承与私有继承相似,都将基类的公有和保护成员在派生类中调整为保护成员。这样的设计使得这些成员对派生类及其子类的内部成员函数是可访问的,但对派生类的对象和其他外部实体仍然是不可见的。同样,基类的私有成员在派生类中也是不可访问的

  • 派生类的构造与析构

  • 构造函数执行顺序:当创建一个派生类对象时,首先会调用基类的构造函数来初始化基类部分的成员变量,这是因为派生类包含了基类的所有数据和功能。这一过程按照继承链,从最远的基类开始,逐层向下,直到最终执行派生类本身的构造函数。这意味着派生类构造函数内的初始化不仅能够访问到自己的成员,还能看到已经初始化好的基类成员。

  • 析构函数执行顺序:与构造过程相反,在对象生命周期结束,需要释放资源时,析构函数的调用顺序则是从派生类开始,向上至基类。也就是说,先执行派生类的析构函数,做清理工作,然后依次是任何嵌入的子对象的析构函数,最后是基类的析构函数。这样的逆序确保了每个类的资源在其构造函数创建的对应资源之后被恰当地销毁。

换种说法,虚基类就像是在家族谱系中设置了一个共享祖先节点,确保了无论后代分支如何繁衍交织,这个共同祖先的遗产(即基类中的数据成员和函数)都只有一份,被所有相关后代以唯一且一致的方式继承。当一个类声明采用虚继承(使用virtual关键字)时,它相当于做出了一个协定,表明自己愿意与其他派生类和谐共用那个虚基类的资源。

在这个体系中,如果派生类自己也定义了与虚基类中同名的成员,根据就近原则,派生类自己的成员会覆盖或隐藏掉从虚基类继承来的同名成员,拥有更高的访问优先级。这意味着,虽然虚基类确保了基类成员的唯一性,但在直接性上,派生类自身的定义总是更胜一筹。

语法上,声明一个使用虚继承的派生类看起来像这样:class 派生类名 : virtual 继承方式 基类名,其中virtual关键字明确了继承关系中的共享意图。

多重继承

在C++中,多重继承是一种特性,允许一个类从多个基类继承属性和行为。这意味着一个类可以同时具有多个基类的所有特性。

以下是一个多重继承的例子:

class Base1 {
public:
    void function1() {}
};

class Base2 {
public:
    void function2() {}
};

class Derived : public Base1, public Base2 {
    // Derived 类继承了 Base1 和 Base2 的所有公有成员
};

int main() {
    Derived d;
    d.function1();  // 来自 Base1 的函数
    d.function2();  // 来自 Base2 的函数
    return 0;
}

在这个例子中,Derived类从Base1Base2类继承。这意味着Derived类的对象可以访问Base1Base2的所有公有成员。

然而,多重继承也有一些问题。例如,如果两个基类有同名的成员,那么在派生类中就会产生歧义。此外,如果两个基类都有一个虚函数,那么在派生类中必须重写这个虚函数,否则会产生编译错误。

因此,虽然多重继承在某些情况下可能很有用,但在许多情况下,使用接口(纯虚函数)或组合可能是更好的选择。

 组合

组合是另一种常用的设计技术,它允许你将一个或多个对象组合到另一个对象中。这样,你可以复用和组合已有的类,而不是通过继承来扩展它们。

class Engine {
public:
    void start() {}
};

class Car {
public:
    void start() {
        engine.start();
    }

private:
    Engine engine;  // Car 类包含一个 Engine 类的对象
};

在这个例子中,Car类包含一个Engine类的对象。这意味着Car类可以使用Engine类的功能,但它并没有从Engine类继承。这是一种组合的例子。

虚函数

在C++中,实现多态的关键技术之一是利用虚函数,它允许使用基类的指针或引用来调用实际对象(可能是派生类对象)中的相应函数版本。这增强了代码的灵活性和可扩展性。

换种表达,虚函数是实现运行时多态的核心机制,它的工作原理如下:当你在基类中用virtual关键字声明一个函数时,实际上是在告诉编译器,“这个函数在派生类中可能会有不同的实现”。因此,任何从该基类派生出的类中,如果有同名函数,不论是否明确标有virtual,都会被视为虚函数,并且能够被动态绑定,即在运行时根据对象的实际类型来决定调用哪个版本的函数。

值得注意的是,虚函数的设计并不适用于构造函数,因为构造函数负责初始化对象,必须在对象创建之初就被确定调用哪个类的构造函数,不能等到运行时才决定。而析构函数可以且常常被声明为虚函数,确保当通过基类指针删除派生类对象时,能够正确调用到派生类的析构函数,完成所有层次资源的释放。

总之,虚函数机制通过基类指针或引用来间接调用派生类中的同名函数,实现了动态多态性,增强了代码的通用性和可维护性。声明格式简单明了,只需在基类函数前加上virtual关键字即可。

抽象类:

包含纯虚函数的类称为抽象类。由于纯虚函数不能被调用,所以不能利用抽象类创建对象,又称抽象基类。就是不能直接创建那个有纯虚函数的类。 

 运算符重载

重载,简而言之,就是在同一作用域内,用相同的名称但因参数列表或类型的不同,而实现不同功能的一种技术。这就好比同一名字的人在不同情境下扮演不同角色,各司其职。

具体到编程中,函数重载意味着一个函数名可以对应多种不同的实现,依据调用时提供的参数类型、数量或顺序,执行相应的逻辑操作,就好比一个多功能的工具箱,根据不同的螺丝选择合适的扳手。

而运算符重载,则是这种多面手概念在操作符上的应用。原本预定义的运算符(如+、-等)通过特殊的函数定义,被赋予了处理自定义类型的能力。就像“+”号,在数学里加数字,在编程中,通过重载,它可以用来连接字符串、加法运算于自定义类对象等,使得运算符能够适应更加丰富多变的场景,增加了代码的可读性和表达力。所以说,运算符重载本质上也是利用函数重载的原理,只不过这次“主角”换成了我们熟悉的运算符。

我们可以对大于小于号重载,使可以让自定义类比较,也可以对流插入和流输出重载使得可以按自己的要求输出自定义类型

特别注意=号对类使用不重载的话会浅拷贝造成错误

下面举几个例子

#include <bits/stdc++.h>
using namespace std;
class test
{
public:
    test(int high,int weight)
        : high(high),weight(weight)
    {}
    int high;
    int weight;
    int operator>(const test& t)
    {
        if (this->high > t.high)
            return this->high > t.high;
        else if (this->high == t.high)
            return this->weight > t.weight;
    }
    friend ostream& operator<<(ostream& out, const test& a);
    friend istream& operator>>(istream& is, test& a);
};
ostream& operator<<(ostream& out, const test& a)
{
    out << a.high << " " << a.weight;
    return out;
}
istream& operator>>(istream& is, test& a)
{
    is >> a.high >> a.weight;
    return is;
}
int main()
{
    test a(100, 400);
    test b(100, 300);
    if (a > b)
    {
        cout << "win" << '\n';
    }
    cout << a << " " << b << '\n';
    return 0;
}

输出结果如下 

运算符重载这些还能对一些private的数组数据来输入 

例如

#include <bits/stdc++.h>
using namespace std;
class test
{
private:
    int a[10];
public:
    int& operator[](int index)
    {
        return a[index];
    }
};
int main()
{
    test a;
    for (int i = 0; i < 10; i++)
    {
        a[i] = i;
    }
    return 0;
}

所以说运算符重载是c++的一大亮点,极大的便捷了做一些事情 

函数重载 

函数重载是C++中的一个特性,它允许你定义多个同名的函数,只要它们的参数列表不同即可。

这意味着你可以根据函数的参数类型和数量来调用不同的函数。 对于operator<<和operator>>,你可以为每个自定义类型重载这些运算符。这样,你就可以根据对象的类型来调用正确的函数。 以下是一个例子,展示了如何为两个不同的类重载operator<<:

#include <iostream>

class A {
public:
    int x;
};

class B {
public:
    double y;
};

std::ostream& operator<<(std::ostream& out, const A& a) {
    out << a.x;
    return out;
}

std::ostream& operator<<(std::ostream& out, const B& b) {
    out << b.y;
    return out;
}

int main() {
    A a{10};
    B b{20.5};
    std::cout << a << '\n';  // 输出:10
    std::cout << b << '\n';  // 输出:20.5
    return 0;
}

当然最普通的add等函数就不用说了 

泛型编程

在C++中,泛型编程是一种编程范式,它允许你编写可以处理多种数据类型的代码,而不需要为每种数据类型都编写单独的代码。泛型编程主要通过模板来实现。

以下是一个简单的C++模板函数的例子:

template <typename T>
T max(T a, T b) {
    return (a > b) ? a : b;
}

sort函数重载

对比c语言的qsort函数,qsort也可以比较结构体的大小,按照我们自己想要的方式

举几个例子

vector<pair<int, int>> cards(18);
sort(cards.begin(), cards.begin() + N, [](const pair<int, int>& a, const pair<int, int>& b) {
    return a.first < b.first;
    });

当然也可以写出来一个函数 

struct Card {
    int index;
    int strength;
    int cost;
};

bool compare(const Card& a, const Card& b) {
    if (a.strength != b.strength) {
        return a.strength > b.strength;
    }
    else {
        return a.cost < b.cost;
    }
}
sort(cards.begin(), cards.end(), compare);

 inline 

这是一个小特性

在C++中,inline关键字用于建议编译器将函数的定义插入到每个调用该函数的地方,这被称为内联函数。这样可以减少函数调用的开销,因为不需要跳转到函数,然后再跳回来。然而,这可能会增加最终生成的代码的大小,因为函数的代码可能会在多个地方复制。

需要注意的是,inline只是一个建议,编译器可以选择忽略它。例如,如果函数非常复杂,编译器可能会选择不进行内联。

另外,所有在类定义中定义的函数默认都是内联函数。

动态数组开辟

c++里面使用的是new和delete函数,对比c语言的malloc free calloc有几个优势

类型安全:

new和delete是类型安全的,它们会返回正确的类型,而不是void*。这意味着不需要进行类型转换。

构造和析构:

new会自动调用对象的构造函数,delete会自动调用对象的析构函数。这对于管理资源非常有用,特别是当对象拥有如文件句柄或网络连接等资源时。

异常:

如果new无法分配所需的内存,它会抛出一个bad_alloc异常。在C语言中,malloc和calloc在无法分配内存时返回NULL,需要手动检查。

运算符重载:

C++允许重载new和delete运算符,以提供自定义的内存管理行为。

更好的内存对齐支持: new提供了更好的内存对齐支持,这对于某些硬件架构来说是必要的。

方便的数组分配:

new[]和delete[]提供了一种方便的方式来分配和释放动态数组。 

总结

然而,使用new和delete也需要注意一些问题,如内存泄漏、异常安全等。在现代C++编程中,通常推荐使用智能指针(如std::unique_ptr和std::shared_ptr)来自动管理内存,以避免这些问题。 

注意 

有时候释放含class的动态内存的时候,不包含[]不会出错,那是因为我们没写析构函数,如果写了析构函数那么给class开的内存的前面四个字节是保存数据大小的,那么就会报错,所以我们写的时候还是要严谨,注意一一对应.

static函数

在类的成员函数前面使用 static 关键字会使得这个函数成为静态成员函数。静态成员函数不依赖于类的任何对象,它可以在没有类的对象的情况下被调用。静态成员函数只能访问类的静态成员,不能访问类的非静态成员。

class MyClass {
public:
    static void foo() { /* ... */ }  // 静态成员函数
};

MyClass::foo();  // 不需要 MyClass 的对象就可以调用

匿名对象

匿名对象是没有名称的类的实例。这些对象在创建后立即使用,并在使用后立即销毁。下面是一个例子:

class MyClass {

public:

    void foo() { std::cout << "Hello, world!" << std::endl; }

};

int main() {

    MyClass().foo();  // 创建 MyClass 的匿名对象并调用其 foo 方法

    return 0;

}

内部类

在 C++ 中,一个类可以在另一个类的内部定义,这种类被称为内部类或嵌套类。内部类的定义在外部类的作用域内,因此它可以访问外部类的所有成员,包括私有成员。然而,外部类不能直接访问内部类的成员。

下面是一个内部类的例子:

class OuterClass {
private:
    int x;

public:
    class InnerClass {
    public:
        void foo(OuterClass& outer) {
            std::cout << outer.x << std::endl;  // 可以访问外部类的私有成员
        }
    };
};

计算相差天数

这种学习的常见问题建议背一个模板

	int operator-(const Time& other) const {
		struct tm a = { 0,0,0,day,month - 1,year - 1900 };
		struct tm b = { 0,0,0,other.day,other.month - 1,other.year - 1900 };
		time_t x = mktime(&a);
		time_t y = mktime(&b);
		double diff = difftime(y, x) / (60 * 60 * 24);
		return abs(diff);
        }
		/*首先,它创建了两个 struct tm 结构体,这是 C 语言中表示时间的结构体。struct tm 包含了年、月、日、时、分、秒等字段。
		在这里,我们只关心年、月、日,所以时、分、秒都被设置为 0。注意,tm 结构体中的月份是从 0 开始的,所以我们需要将月份减 1。另外,年份是从 1900 年开始的,所以我们需要将年份减 1900。
		然后,它使用 mktime 函数将 struct tm 转换为 time_t。time_t 是一个表示时间的整数类型,它表示的是从 1970 年 1 月 1 日 00 :00 : 00 UTC 到指定时间的秒数。
		接下来,它使用 difftime 函数计算两个 time_t 之间的差值。difftime 返回的是两个时间之间的秒数。
		最后,它将秒数转换为天数(因为一天有 24 小时,一小时有 60 分钟,一分钟有 60 秒),并返回这个天数。如果天数是负数(也就是说,第一个日期晚于第二个日期),那么它会返回天数的绝对值。*/

  • 13
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值