C++知识点整理

C++

#define

宏定义可以实现类似于函数的功能,但是它终归不是函数,而宏定义中括弧中的“参数”也不是真的参数,在宏展开的时候对 “参数” 进行的是一对一的替换。

特点:

1.宏定义是C和C++语言都支持的一种预处理指令

2.宏定义是由预处理器实现的,宏定义的调用是没有类型检查的。这意味着在使用宏定义时,需要保证参数类型的正确性,否则会导致编译错误或者程序运行时错误。

3.宏定义的代码是在预处理阶段直接替换为代码,无法进行调试

4.宏定义中的参数和返回值都是文本替换,不支持类型检查和特性

5.宏定义没有这些限制,它会在所有地方进行文本替换,因此可能会增加代码的体积和复杂度。

assert

断言,是宏,而非函数。
其作用是如果它的条件返回错误,则终止程序执行。

引用&

引用是 C++ 中的一个重要特性,它提供了一种直接访问对象的方式,类似于变量的别名。引用提供了一种简洁、直观的方法来操作变量,能够提高代码的可读性和效率。

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

int num = 10;
int& ref = num;  // 引用变量 ref 是变量 num 的别名复制Error复制成功...

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

初始化和赋值

引用在声明时必须进行初始化,一旦初始化后,它将始终引用同一个对象。引用的初始化可以在声明时进行,也可以在后续赋值操作中进行。

作用域

引用的作用域与变量的作用域相同。引用在定义所在的作用域内有效,超出该作用域后,引用不再有效。

特点

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

用法

可以统一理解为,不用额外复制变量创建内存空间,直接访问需要操作的变量。

1.作为函数参数

2.作为函数返回值

3.和const一起作为函数参数(常量引用)

4.和数组一起作为函数参数

引用可以与数组一起使用,以引用数组的元素或作为数组的别名。

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.和结构体、类一起作为函数参数

应用

1.通过引用修改函数参数

使用引用作为函数参数可以直接修改原始对象,而不是复制对象的副本。这在需要修改函数参数的情况下非常有用。

void modifyValue(int& value) {
    value = 10;
}

int main() {
    int num = 5;
    modifyValue(num);  // 通过引用修改原始对象
    // 现在 num 的值为 10
    return 0;
}

在上述示例中,通过引用将 num 传递给函数 modifyValue,函数直接修改了原始对象 num 的值。

2.函数返回引用的链式操作

链式操作:成员函数返回对象本身。

返回引用的函数可以用于实现链式操作,使代码更加简洁和易读。

class Counter {
private:
    int count;

public:
    Counter(int start = 0) : count(start) {}

    Counter& increment() {
        count++;
        return *this;
    }

    int getCount() const {
        return count;
    }
};

int main() {
    Counter c;
    c.increment().increment().increment();
    cout << c.getCount();  // 输出 3
    return 0;
}

在上述示例中,increment 函数返回对 Counter 对象的引用,使得可以对同一个对象进行多次操作,从而实现链式操作。

3.使用引用提高性能

使用引用可以避免对象的复制,从而提高程序的性能。当对象较大时,通过引用传递参数比通过值传递参数更高效。

void processLargeObject(LargeObject& obj) {
    // 对大对象进行处理
}

int main() {
    LargeObject obj;
    processLargeObject(obj);  // 通过引用传递大对象,避免复制
    return 0;
}

在上述示例中,我们将大对象 obj 通过引用传递给函数 processLargeObject,避免了对象的复制,提高了程序的性能。

4.使用引用参数避免对象复制

当函数需要访问和修改对象的成员时,使用引用参数可以避免对对象进行复制。

void modifyObject(MyObject& obj) {
    obj.setValue(10);
}

int main() {
    MyObject obj;
    modifyObject(obj);  // 通过引用传递对象,避免复制
    return 0;
}

在上述示例中,我们通过引用将对象 obj 传递给函数 modifyObject,函数可以直接访问和修改对象的成员,而无需复制对象。

5.引用作为容器的元素类型

引用可以作为容器(如数组、向量)的元素类型,允许在容器中存储对其他对象的引用。

int num1 = 10;
int num2 = 20;
int& ref1 = num1;
int& ref2 = num2;

vector<int&> refContainer;
refContainer.push_back(ref1);
refContainer.push_back(ref2);

for (int& ref : refContainer) {
    cout << ref << " ";
}

在上述示例中,我们声明了两个整型变量 num1num2,并创建了对它们的引用 ref1ref2。然后,我们创建了一个存储整型引用的向量 refContainer,并将 ref1ref2 添加到容器中。最后,我们使用范围基于循环来遍历容器并输出引用所引用的值。

悬空引用

引用必须始终引用一个有效的对象,否则会产生悬空引用。悬空引用指的是引用一个已被销毁的对象或不存在的对象,这将导致未定义的行为。因此,在使用引用时要特别注意,确保引用所引用的对象的生命周期正确管理。

int& getReference() {
    int num = 10;
    return num;  // 返回局部变量的引用,产生悬空引用
}

int main() {
    int& ref = getReference();
    // 此处引用 ref 是悬空引用,访问引用将导致未定义的行为
    return 0;
}

在上述示例中,我们定义了一个函数 getReference,它返回一个局部变量 num 的引用。然而,当函数返回后,局部变量 num 被销毁,引用 ref 变成了悬空引用,访问引用将导致未定义的行为。

右值引用

C++11增加了一个新的类型,称为右值引用(R-value reference),标记为 &&。在介绍右值引用类型之前要先了解什么是左值和右值:

  • lvalue是locator value的缩写,rvalue是 read value的缩写。
  • 左值是指存储在内存中、有明确存储地址(可取地址)的数据。
  • 右值是指可以提供数据值的数据(不可取地址)。

通过描述可以看出,区分左值与右值的便捷方法是:可以对表达式取地址(&)就是左值,否则为右值。

右值引用就是对一个右值进行引用的类型。因为右值是匿名的,所以我们只能通过引用它的方式找到它。无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。通过右值引用的声明,该右值又”重获新生”。

其生命周期与右值引用类型变量的声明周期一样,只要该变量还活着,该右值临时量将会一直存活下去。

引用和指针

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

  • 引用是一个别名,一旦初始化后不可改变,总是引用同一个对象,而指针可以改变所指向的对象。
  • 引用不需要使用解引用操作符(*)来访问所引用的对象,而指针需要使用解引用操作符来访问所指向的对象。
  • 引用在声明时必须初始化,并且不能引用空值(NULL),而指针可以在声明后再进行初始化,并且可以指向空值。
  • 引用不需要进行内存分配和释放,而指针需要手动进行内存管理。

关键字

const

const叫常量限定符,可以修饰变量、指针*、引用&和成员函数
1.当修饰变量时,例如:

const int Max = 100;//等同与int const Max = 100;
int Array[Max];

在C中运行会报错,说明const修饰的是只读变量而不是常量。但是C++拓展了const的含义,并不会报错。

int const a[5]={1,2,3,4,5};//同样可以修饰只读数组

2.当修饰指针时,需要知道不可变的是指针本身还是指针指向的对象:

int const *p;//等同于const int *p;不可变的是指针指向的对象。
int * const p;//指针本身不可变

int const * const p;//指针本身和指针指向的对象都不可变

3.当修饰引用时,两种情况都一样

int const &a=x;//等同于const int &a=x;
int &const a=x;//这种方式定义是C、C++编译器未定义,虽然不会报错,但是该句效果和int &a一样。
//用于形参类型,即避免了拷贝,又避免了函数对值的修改

4.当修饰函数时,说明该成员函数内不能修改成员变量

//const修饰函数参数
void fun0(const A* a);//不能对传递进来的指针的内容进行改变,保护了原指针所指向的内容
void fun1(const A& a);//不能对传递进来的引用对象进行改变,保护了原对象的属性

//const修饰函数返回值
const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}//一般用const修饰返回值为对象本身(非引用和指针)的情况多用于二目操作符重载函数并产生新对象的时候。

5.思考

// 类
class A
{
private:
    const int a;                // 常对象成员,可以使用初始化列表或者类内初始化

public:
    // 构造函数
    A() : a(0) { };
    A(int x) : a(x) { };        // 初始化列表

    // const可用于对重载函数的区分
    int getValue();             // 普通成员函数
    int getValue() const;       // 常成员函数,不得修改类中的任何数据成员的值
};

void function()
{
    // 对象
    A b;                        // 普通对象,可以调用全部成员函数
    const A a;                  // 常对象,只能调用常成员函数
    const A *p = &a;            // 指针变量,指向常对象
    const A &q = a;             // 指向常对象的引用

    // 指针
    char greeting[] = "Hello";
    char* p1 = greeting;                // 指针变量,指向字符数组变量
    const char* p2 = greeting;          // 指针变量,指向字符数组常量(const 后面是 char,说明指向的字符(char)不可改变)
    char* const p3 = greeting;          // 自身是常量的指针,指向字符数组变量(const 后面是 p3,说明 p3 指针自身不可改变)
    const char* const p4 = greeting;    // 自身是常量的指针,指向字符数组常量
}

// 函数
void function1(const int Var);           // 传递过来的参数在函数内不可变
void function2(const char* Var);         // 参数指针所指内容为常量
void function3(char* const Var);         // 参数指针为常量
void function4(const int& Var);          // 引用参数在函数内为常量

// 函数返回值
const int function5();      // 返回一个常数
const int* function6();     // 返回一个指向常量的指针变量,使用:const int *p = function6();
int* const function7();     // 返回一个指向变量的常指针,使用:int* const p = function7();

使用 const 的好处:

  • 提高代码的可读性:通过明确标识常量和只读的对象,使代码更易于理解和维护。
  • 防止意外修改:将对象声明为 const 可以防止在代码中无意中修改其值,提高代码的稳定性和可靠性。
  • 编译器优化:编译器可以使用 const 信息进行优化,提高代码的执行效率。

需要注意的是,const 对象必须在声明时进行初始化,并且其值在编译时就确定了,不能在运行时修改。

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

const和#define的区别

#define const
宏定义 常量声明
预处理器处理 编译器处理
无类型安全检查 有类型安全检查
不分配内存 要分配内存
存储在代码段 存储在数据段
可通过#undef取消 不可取消

1.#define不是关键字,而是一个预处理指令。它是宏定义,相当于字符替换。
2.const效率比#define高。因为编译器通常不为普通const分配存储空间,而是保存在符号表(数据段)中,所以const没有了存储和读内存的操作。
3.#define宏没有类型,const有特定类型。
4.#define在预处理时不分配内存,而const也不分配内存。但是定义变量时const只有第一次会分配内存(只有一份备份),而#define定义一个变量就分配一次内存(若干个备份)

static

static可以修饰变量、函数、成员变量和成员函数

1.修饰变量时,修改变量的存储区域和生命周期,变量存储在静态区,在 main 函数运行前就分配了空间,如果有初始值就用初始值初始化它,如果没有初始值系统用默认值初始化它。

2.修饰普通函数,表明函数的作用范围,仅在定义该函数的文件内才能使用。在多人开发项目时,为了防止与他人命名空间里的函数重名,可以将函数定位为 static。

C++补充:

3.修饰成员变量,修饰成员变量使所有的对象只保存一个该变量,而且不需要生成对象就可以访问该成员

4.修饰成员函数,修饰成员函数使得不需要生成对象就可以访问该函数,但是在 static 函数内不能访问非静态成员

volatile

volatile int i = 10; 

1.volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素(操作系统、硬件、其它线程等)更改。所以使用 volatile 告诉编译器不应对这样的对象进行优化
2.volatile 关键字声明的变量,每次访问时都必须从内存中取出值(没有被 volatile 修饰的变量,可能由于编译器的优化,从 CPU 寄存器中取值)
3.const 可以是 volatile (如只读的状态寄存器)
4.指针可以是 volatile

sizeof

注意它不是一个函数,而是关键字,不加括号也可以,但是通常加括号。

int i = 0;
sizeof(int);//4
sizeof(i);//4
sizeof int;//报错,int前面不能加关键字sizeof
sizeof i;//4
//sizeof 对数组,得到整个数组所占空间大小。
int a[100];
sizeof(a);//400
sizeof(a[100]);//4
sizeof(&a);//4
sizeof(&a[0]);//4

//sizeof 对指针,得到指针本身所占空间大小。
int *p = NULL;
sizeof(p);//4
sizeof(*p);//4
sizeof(int) *p;//报错

#pragma pack

内存对齐的参数。
设定结构体、联合以及类成员变量以 n 字节方式对齐

#pragma pack(push)  // 保存对齐状态
#pragma pack(4)     // 设定为 4 字节对齐

struct test
{
    char m1;
    double m4;
    int m3;
};

#pragma pack(pop)   // 恢复对齐状态

extern

表明变量或函数定义在其他文件中。
它没有定义一个变量,更像是声明一个变量。

C++补充:
被extern "C"修饰的变量和函数是按照 C 语言方式编译和链接的。

#ifdef __cplusplus
extern "C" {
#endif

void *memset(void *, int, size_t);

#ifdef __cplusplus
}
#endif

struct

1.需要注意.和->的含义和区别:
.是用于访问结构体的成员变量
->是用于访问结构体指针的成员变量

2.常和typedef联合使用。

C++补充:
3.定义变量时,struct可省略
4.Student是结构体同时也是函数时,Student只代表函数。

class

类包括属性和方法

class 类名{
	public://公共的行为或属性
	
	private://私密的行为或属性
};

private 表示该部分内容是私密的, 不能被外部所访问或调用, 只能被本类内部访问; 而 public 表示公开的属性和方法, 外界可以直接访问或者调用。

struct和class的区别

1.使用 class 时,类中的成员默认都是 private 属性的;而使用 struct 时,结构体中的成员默认都是 public 属性的。

  1. class 继承默认是 private 继承,而 struct 继承默认是 public 继承。
  2. class 可以使用模板,而 struct 不能。

在编写C++代码时,要使用 class 来定义类,使用 struct 来定义结构体,使语义更加明确。

union(不太懂)

联合(union)是一种节省空间的特殊的类,一个 union 可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当某个成员被赋值后其他成员变为未定义状态。
1.默认访问控制符为 public
2.可以含有构造函数、析构函数
3.不能含有引用类型的成员
4.不能继承自其他类,不能作为基类
5.不能含有虚函数
6.匿名 union 在定义所在作用域可直接访问 union 成员
7.匿名 union 不能包含 protected 成员或 private 成员
8.全局匿名联合必须是静态(static)的

this

它是一个const指针,是所有成员函数的隐含参数。
this 只能用在类的内部,可用于调用类的成员函数和成员变量。

inline

相当于把内联函数里面的内容写在调用内联函数处;
相当于不用执行进入函数的步骤,直接执行函数体;
类似于宏,却比宏多了类型检查,真正具有函数特性;
编译器一般不内联包含循环、递归、switch 等复杂操作的内联函数;
在类声明中定义的函数,除了虚函数的其他函数都会自动隐式地当成内联函数。

explicit(不太懂)

explicit 修饰构造函数时,可以防止隐式转换和复制初始化
explicit 修饰转换函数时,可以防止隐式转换,但 按语境转换 除外

friend

一句话:一种可以访问private权限的成员 的 方法

私有成员只能在类的成员函数内部访问,如果想在别处访问对象的私有成员,只能通过类提供的接口(成员函数)间接地进行,这固然能够带来数据隐藏的好处,利于将来程序的扩充,但也会增加程序书写的麻烦

友元可以看成是现实生活中的 好闺蜜 或者是 好基友

友元类和友元函数
能访问私有成员
破坏封装性
友元关系不可传递
友元关系的单向性
友元声明的形式及数量不受限制

using

using分为using声明和using指示。
1.using声明(多用)
一条 using 声明 语句一次只引入命名空间的一个成员。它使得我们可以清楚知道程序中所引用的到底是哪个名字

2.using指示(少用)(不太懂)
using 指示 使得某个特定命名空间中所有名字都可见,这样我们就无需再为它们添加任何前缀限定符了。

using指示会污染命名空间

using声明,例如:

using namespace std//是指开辟一个新的命名空间(作用域),namespace也是一个关键字

enum(不太懂)

限定作用域的枚举类型

enum class open_modes { input, output, append };

不限定作用域的枚举类型

enum color { red, yellow, green };
enum { floatPrec = 6, doublePrec = 10 };

decltype

decltype 关键字用于检查实体的声明类型或表达式的类型及值分类。

initializer_list

用花括号初始化器列表初始化一个对象,其中对应构造函数接受一个 std::initializer_list 参数

override

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

override 并不会影响程序的执行结果,仅仅是作用于编译阶段,用于检查子类是否真的重写父类函数

final

一个特殊的关键字,有2个作用

  • 禁止虚函数被重写
  • 禁止类被继承

注意:

  • 只有虚函数才能被标记为final ,其他的普通函数无法标记final

运算符

运算符优先级

(单目)(算术)(移位)(比较),(按位)(逻辑)(三目)(赋值)

单算移比,按逻三赋

C++运算符的优先级如下:

括号中的表达式具有最高优先级。

成员选择运算符(.)和成员指针运算符(->)的优先级较高。

递增(++)和递减(--)运算符的优先级较高。

乘法(*)、除法(/)和取模(%)运算符的优先级较高。

加法(+)和减法(-)运算符的优先级较低于乘法、除法和取模运算符。

移位运算符(<<和>>)的优先级较低于加法和减法运算符。

关系运算符(<、<=、>、>=)的优先级较低于移位运算符。

相等性运算符(==和!=)的优先级较低于关系运算符。

位运算符(&、|和^)的优先级较低于相等性运算符。

逻辑运算符(&&和||)的优先级较低于位运算符。

条件运算符(三目运算符:?)的优先级较低于逻辑运算符。

赋值运算符(=、+=、-=等)的优先级较低于条件运算符。

域运算符::

定义

其实就是限制作用域,比如A和B两个类都有member这个属性,而

A::member;//指类A中的member成员属性
B::member;//指类B中的member成员属性

用法

范围解析运算符(作用域分解运算符)
1.全局作用域符(::name):用于类型名称(类、类成员、成员函数、变量等)前,表示作用域为全局命名空间
2.类作用域符(class::name):用于表示指定类型的作用域范围是具体某个类的
3.命名空间作用域符(namespace::name):用于表示指定类型的作用域范围是具体某个命名空间的

int count = 11;         // 全局(::)的 count

class A {
public:
	static int count;   // 类 A 的 count(A::count)
};
int A::count = 21;

void fun()
{
	int count = 31;     // 初始化局部的 count 为 31
	count = 32;         // 设置局部的 count 的值为 32
}

int main() {
	::count = 12;       // 测试 1:设置全局的 count 的值为 12

	A::count = 22;      // 测试 2:设置类 A 的 count 为 22

	fun();		        // 测试 3

	return 0;
}

强制类型转换运算符

static_cast

  • 用于非多态类型的转换
  • 不执行运行时类型检查(转换安全性不如 dynamic_cast)
  • 通常用于转换数值数据类型(如 float -> int)
  • 可以在整个类层次结构中移动指针,子类转化为父类安全(向上转换),父类转化为子类不安全(因为子类可能有不在父类的字段或方法)

向上转换是一种隐式转换。

dynamic_cast

  • 用于多态类型的转换
  • 执行行运行时类型检查
  • 只适用于指针或引用
  • 对不明确的指针的转换将失败(返回 nullptr),但不引发异常
  • 可以在整个类层次结构中移动指针,包括向上转换、向下转换

const_cast

  • 用于删除 const、volatile 和 __unaligned 特性(如将 const int 类型转换为 int 类型 )

reinterpret_cast

  • 用于位的简单重新解释
  • 滥用 reinterpret_cast 运算符可能很容易带来风险。 除非所需转换本身是低级别的,否则应使用其他强制转换运算符之一。
  • 允许将任何指针转换为任何其他指针类型(如 char*int*One_class*Unrelated_class* 之类的转换,但其本身并不安全)
  • 也允许将任何整数类型转换为任何指针类型以及反向转换。
  • reinterpret_cast 运算符不能丢掉 const、volatile 或 __unaligned 特性。
  • reinterpret_cast 的一个实际用途是在哈希函数中,即,通过让两个不同的值几乎不以相同的索引结尾的方式将值映射到索引。

bad_cast

  • 由于强制转换为引用类型失败,dynamic_cast 运算符引发 bad_cast 异常。

bad_cast 使用

try {  
    Circle& ref_circle = dynamic_cast<Circle&>(ref_shape);   
}  
catch (bad_cast b) {  
    cout << "Caught: " << b.what();  
} 

运行时类型信息 (RTTI)

dynamic_cast

  • 用于多态类型的转换

typeid

  • typeid 运算符允许在运行时确定对象的类型
  • type_id 返回一个 type_info 对象的引用
  • 如果想通过基类的指针获得派生类的数据类型,基类必须带有虚函数
  • 只能获取对象的实际类型

type_info

  • type_info 类描述编译器在程序中生成的类型信息。 此类的对象可以有效存储指向类型的名称的指针。 type_info 类还可存储适合比较两个类型是否相等或比较其排列顺序的编码值。 类型的编码规则和排列顺序是未指定的,并且可能因程序而异。
  • 头文件:typeinfo

typeid、type_info 使用

#include <iostream>
using namespace std;

class Flyable                       // 能飞的
{
public:
    virtual void takeoff() = 0;     // 起飞
    virtual void land() = 0;        // 降落
};
class Bird : public Flyable         // 鸟
{
public:
    void foraging() {...}           // 觅食
    virtual void takeoff() {...}
    virtual void land() {...}
    virtual ~Bird(){}
};
class Plane : public Flyable        // 飞机
{
public:
    void carry() {...}              // 运输
    virtual void takeoff() {...}
    virtual void land() {...}
};

class type_info
{
public:
    const char* name() const;
    bool operator == (const type_info & rhs) const;
    bool operator != (const type_info & rhs) const;
    int before(const type_info & rhs) const;
    virtual ~type_info();
private:
    ...
};

void doSomething(Flyable *obj)                 // 做些事情
{
    obj->takeoff();

    cout << typeid(*obj).name() << endl;        // 输出传入对象类型("class Bird" or "class Plane")

    if(typeid(*obj) == typeid(Bird))            // 判断对象类型
    {
        Bird *bird = dynamic_cast<Bird *>(obj); // 对象转化
        bird->foraging();
    }

    obj->land();
}

int main(){
	Bird *b = new Bird();
	doSomething(b);
	delete b;
	b = nullptr;
	return 0;
}

运算符重载

函数可以重载, 运算符也是可以重载的。 运算符重载是对已有的运算符重新进行定义,赋予其另一种功能,以达到适应不同的数据类型。运算符重载不能改变它本来的寓意(也就是加法不能变更为减法)。更像是拓展某一个运算符的功能???

运算符的重载实际上背后是调用对应的函数,重载运算符使得把复杂的代码包装起来,对外暴露简单的一个符号即可。

定义

重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。

重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的

比如,要重载+ 运算符 ,那么可以声明一个函数为 operator+() ,函数声明的位置可以是类的内部,也可以是类的外部,所以又有了成员函数和全局函数的划分。与其他函数一样,重载运算符函数,也可以拥有返回值和参数列表。此处仍然以学生相加的案例举例。

成员函数方式

把重载的运算符函数定义在类中,此时只需要接收一个参数,因为类的对象本身作为+ 的前面调用者

#include<iostream>

using namespace std;

class Student {
public:
    int age;

    Student(int age) : age(age) {

    }

    //两个学生的年龄之和,则为第三个学生的命令,所以此处需要返回一个学生对象。
    //好方便在外面接收。
    Student operator+(Student &s) {
        Student temp(this->age + s.age);
        return temp;
    }

};

int main() {

    Student s1(10);
    Student s2(20);

    //这里等于使用s1的对象,调用了operator+这个函数, +后面的 s2 则被当成参数来传递
    Student s3 = s1 + s2;

    cout << "s3.age = " << s3.age << endl;

    return 0;
}

全局函数方式

并不是所有的运算符重载都能定义在类中,比如,需要扩展一些已有的类,对它进行运算符重载,而这些类已经被打成了一个库来使用(无法修改类的内部成员函数),此时通过全局函数的方式来实现运算符重载

#include<iostream>

using namespace std;

class Student {
public:
    int age;

    Student(int age) : age(age) {

    }
};

//由于函数并非定义在类中,所以此处无法使用到this指针,则需要传递两个对象进来。
Student operator+(Student &s, Student &ss) {
    Student temp(s.age + ss.age);
    return temp;
}

int main() {

    Student s1(20);
    Student s2(30);

    //这里等于使用s1的对象,调用了operator+这个函数, +后面的 s2 则被当成参数来传递
    Student s3 = s1 + s2;

    cout << "s3.age = " << s3.age << endl;
    return 0;
}

赋值运算符=重载

(拷贝赋值)当两个已经存在的同类型对象之间的相互赋值 ,调用的就是拷贝赋值。

如果一个类没有实现拷贝赋值函数,编译器提供默认的拷贝赋值函数,且为浅拷贝

若需要实现深拷贝,则需要程序员手动提供。

#include <iostream>

using namespace std;


class Student {
public:
    int no;
    int age;

    // 构造函数
    Student(int no, int age) {
        cout << this << "构造函数被执行...\n";
        this->no = no;
        this->age = age;
    }

    // 拷贝构造
    Student(const Student &stu) {
        this->no = stu.no;
        this->age = stu.age;
        cout << this << "拷贝构造函数被执行...\n";
    }

    // 拷贝赋值
    Student &operator=(const Student &stu) {
        cout << this << "拷贝赋值函数被执行...\n";
        this->no = stu.no;
        this->age = stu.age;
    }
};


int main() {

    Student s1(10001, 15);
    Student s2(s1);  // 此处执行的是拷贝构造函数

    s2 = s1;  // 此处执行的是拷贝赋值函数

    return 0;
}

深拷贝和浅拷贝

浅拷贝:按位复制内存。将A和B所在内存中的数据按照二进制位(Bit)复制到 A 和 B 所在的内存,这种默认的拷贝行为就是浅拷贝,这和调用 memcpy() 函数的效果非常类似。

对于简单的类,会自动生成拷贝构造函数。但是当该类持有特殊的资源时,比如动态分配的内存、指向其他数据的指针等,自动生成的拷贝构造函数就不会拷贝这些资源。这个时候,就需要我们手动并显式地定义可以拷贝这些资源的拷贝构造函数,即进行深拷贝。

深拷贝:将对象所持有的其它资源一并拷贝的行为叫做深拷贝,我们必须显式地定义拷贝构造函数才能达到深拷贝的目的。

调用运算符()重载

一般来说,可以使用对象来访问类中的成员函数,而对象本身是不能像函数一样被调用的,除非在类中重载了调用运算符。 如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象。在外面使用对象(),实际上背后访问的是类中重载的调用运算符函数。

如果某个类重载了调用运算符,那么该类的对象即可称之为:函数对象 ,因为可以调用这种对象,所以才说这些对象行为像函数一样。

#include<iostream>

using namespace std;

class Calc {
public:
    int operator()(int val) {
        return val < 0 ? -val : val;
    }
};

int main() {
    Calc c;
    int value = c(-10);

    cout << value << "\n";

    return 0;
}

运算符重载规则

//不能重载,5个:".", ".*", "::", "sizeof", "?:"
前两个不能被重载是保证能被正常访问成员,域运算和sizeof不能被重载是因为运算对象是类而不是对象。

//只能使用成员函数重载,4个:"=","[]","->","()"
防止出现:1 = x,1->x,1(x)这样的语句

类和对象

面向对象三大特征 —— 封装、继承、多态
image

this指针

this 是 C++ 中的一个关键字,它是一个指针,指向当前对象,通过它可以访问当前对象的所有成员

说的直白一点:在成员函数被调用的过程中,会自动生成1个this指针变量,它指向当前调用对象自己,因此可以直接使用this->操作成员变量与成员函数。

this 实际上是成员函数的一个形参,在调用成员函数时将对象的地址作为实参传递给 this。不过 this 这个形参是隐式的,它并不出现在代码中,而是在编译阶段由编译器默默地将它添加到参数列表中。

本质上,this 作为隐式形参,是成员函数的局部变量,所以只能用在成员函数的内部,并且只有在通过对象调用成员函数时才给 this 赋值

抽象类

包含纯虚函数的类称为抽象类(Abstract Class)

之所以说它抽象,是因为它无法实例化,也就是无法创建对象,原因纯虚函数没有函数体不是完整的函数无法调用也无法为其分配内存空间

抽象类通常是作为基类,让派生类去实现纯虚函数派生类必须实现纯虚函数才能被实例化

  • 一个纯虚函数就可以使类成为抽象基类,但是抽象基类中除了包含纯虚函数外,还可以包含其它的成员函数(虚函数或普通函数)和成员变量
  • 只有类中的虚函数才能被声明为纯虚函数,普通成员函数和顶层函数均不能声明为纯虚函数

静态成员

程序运行的时候,静态成员已经加载在内存里面了,但是包含静态成员的对象共享这些静态成员。

注意:

1.程序运行的时候,静态成员已经加载在内存里面

2.是类的成员

3.不能说虚函数

4.静态成员函数不能直接访问非静态成员,可以间接的访问,比如通过参数对象和引用

构造函数

构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。

与类名同名,没有返回值,可以被重载

通常用来做初始化工作,比如成员变量初始化等。

当父子类都有构造函数时:

  • 子类构造函数总是需要调用一个父类构造函数;当父类没有无参数的构造函数时,就必须显式指明调用哪一个构造函数;
  • 子类默认自动调用父类无参构造函数,传递参数才会调用有参的构造函数;
  • 当父类没有无参构造函数时,子类必须指定

拷贝构造函数

拷贝构造函数是用另一个对象构造当前对象的时候执行的。

拷贝构造函数就是函数名是当前类的名字,参数为当前类的另一个对象的函数

如果你没有定义拷贝构造函数,编译器会替你合成一个。

绝大多数情况下,使用编译器合成的版本即可。

1.用已有对象构造新对象的时候

Student stu;//先定义一个对象
Student stu2(stu);//(1)会调用拷贝构造函数
Student stu3 = stu ;//(2)会调用拷贝构造函数,这里和(1)是一样的,仅仅是写法不同而已

2.给函数传递值类型参数的时候

void test_function(Student s)
{//s在该函数被调用的时候创建,该函数执行完之后释放
    s.m_name = "李四";//修改s的名字
}
int main()
{
    Student stu("张三");
    test_function(stu);//(1)创建stu的副本,函数内使用拷贝构造函数得到副本
    cout<<stu.m_name;//还是输出“张三”,因为修改的是,函数内的副本
}

3.函数返回值类型对象的时候

Student get_copy(void)
{
    Student s;
    return s;//这里以s为参数构造一个副本,并返回副本,这里发生了拷贝
}

参考资料:https://zhuanlan.zhihu.com/p/382425471

初始化列表

在构造函数中,一种对成员变量初始化的特殊写法,简称初始化列。

// 使用初始化列表,对成员变量进行设置默认值
#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){

    }
};

// 调用父类的有参构造函数
#include <iostream>
using namespace std;
// 父类
class People {
public:
    char *name;
    int age;
    People(char *name, int age) {
        this->name = name;
        this->age = age;
    }
    virtual void display() {
        cout << this->name << "今年" << this->age << "岁了,是个自由从业者" << endl;
    }

};
// 子类
class Teacher : public People {
public:
    int salary;
    Teacher(char *name, int age, int salary) : People(name, age) {
        this->salary = salary;
    }
    void display() {
        cout << this->name << "今年" << this->age << "岁了,是一名教师,每月有" << this->salary << "元的收入" << endl;
    }
};
int main() {
    People *boy = new People("小明", 23);
    boy->display();
    
    People *girl = new Teacher("菲菲", 35, 8200);
    girl->display();
    return 0;
}

构造函数和初始化列表的区别

1.构造函数中初始化是通过在构造函数的函数体内赋值来实现的。

2.成员初始化列表是通过在构造函数的参数列表后面用冒号分隔,然后列出成员变量名和它们的初始值来实现的。

3.使用成员初始化列表会更快一些的原因是因为在构造函数中初始化时,编译器会先调用默认构造函数来初始化成员变量,然后再将初始值赋给它们。这意味着在构造函数体内的赋值语句中,每个成员变量实际上被初始化了两次。而使用成员初始化列表时,成员变量被直接初始化为所需的值,这样就避免了不必要的初始化和赋值操作,从而提高了效率。此外,成员初始化列表还可以初始化const成员变量和引用类型成员变量,而构造函数体内则不能初始化这些成员变量。

4.如果是在构造函数体内进行赋值的话,等于是一次默认构造加一次赋值,而初始化列表只做一次赋值操作。

简单来说就是,在构造函数中初始化时,编译器会先调用构造函数来初始化成员变量,再执行函数体内部的赋值(其实被赋值两次,第一次没有内容而已)。而使用成员初始化列表时,成员变量被直接初始化为所需的值(赋值一次)。

析构函数

一种在对象销毁时,自动调用的函数

析构函数名称与类名称相同,只是在前面加了个波浪号~作为前缀,它不会返回任何值,也不能带有任何参数,不能被重载

一般用于释放资源,

#include <iostream>
#include <string>

using namespace std;

class Student {
public :
	// 成员变量
    char *name;
    int age;

    // 构造函数
    Student() {
		//成员变量初始化
        cout << "执行无参构造函数" << endl;
    }

    Student(char *name) {
        cout << "执行含有一个参数的参构造函数" << endl;
    }

    Student(char *name, int age) {
        cout << "执行含有两个参数的构造函数" << endl;
    }

    // 析构函数
    ~Student() {
        cout << "执行析构函数" << endl;
    }
};

封装

把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。关键字:public, protected, private。不写默认为 private。

说的直白一点就是:一个对象,由很多数据(成员变量)+很多函数(成员函数)组成,有些时候定义的成员变量数据比较重要不想通过对象。成员变量=xxx或对象指针->成员变量=xxx的方式直接修改,此时我们就需要使用到权限的限定。

  • public 成员:可以被任意实体访问(公有的)
  • protected 成员:只允许被子类及本类的成员函数访问(受保护的)
  • private 成员:只允许被本类的成员函数、友元类或友元函数访问(私有的)

在类的内部(定义类的代码内部),无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制

在类的外部(定义类的代码之外),只能通过对象或指针访问public修饰的成员,不能访问 private、protected 修饰的成员

还有一种情况,如果定义了private成员变量,那么可以定义对应的public成员函数,这样做的好处

  • 这个私有的成员变量,不能直接通过外部对象访问,但是可以通过这个公有的函数间接访问

一般给某个私有的成员变量,创建对应的2个公有成员函数

  • 给成员变量赋值的函数通常称为 set 函数,它们的名字通常以set开头,后跟成员变量的名字
  • 读取成员变量的值的函数通常称为 get 函数,它们的名字通常以get开头,后跟成员变量的名字

这种将成员变量声明为 private、将部分成员函数声明为 public 的做法体现了类的封装性

所谓封装,是指尽量隐藏类的内部实现,只向用户提供有用的成员函数。(整体包起来,只提供API接口)

注意:

1.通过友元函数,可以实现类外成员访问,类内私有成员。

继承

基类(父类)——> 派生类(子类)

继承(Inheritance)可以理解为一个类从另一个类获取成员变量和成员函数的过程。

派生和继承是一个概念,只是角度不同。继承是儿子接收父亲的东西,派生是父亲把东西传承给儿子。

被继承的类称为父类或基类,继承的类称为子类或派生类。“子类”和“父类”通常放在一起称呼,“基类”和“派生类”通常放在一起称呼。

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

继承方式包括 public(公有的)、private(私有的)和 protected(受保护的),此项是可选的,如果不写,那么默认为 private。

继承方式public、private和 protected可以修饰成员,当然也可以作为类的继承方式:

// 以public方式继承
#include <iostream>
using namespace std;
class A {
private:
    int a;
protected:
    int b;
public:
    int c;
};
class B : public A {//子类B按public的方式继承A
public:
    void test() {
        this->a = 100;  // 不能用,因为 a是私有权限,不能被继承
        this->b = 100;  // 可以用,因为 虽然b是受保护的权限,但是依然可以被继承
        this->c = 100;  // 可以用,因为 c是公有的权限
    }
};
int main() {
    class B boy;
    boy.test();
    return 0;
}


// 以protected方式继承
#include <iostream>
using namespace std;
class A {
private:
    int a;
protected:
    int b;
public:
    int c;
};
class B : protected A {
public:
    void test() {
        this->a = 100;  // 不能用,因为 a是私有权限,不能被继承
        this->b = 100;  // 可以用,因为 虽然b是受保护的权限,但是依然可以被继承
        this->c = 100;  // 可以用,但此时被修改成了protected权限
    }
};
int main() {
    class B boy;
    boy.test();
    boy.a = 100;  // 不能用,它在父类中定义是用的private权限,都没有被继承,肯定也就用不了
    boy.b = 100;  // 不能用,虽然被子类继承了,但是在父类中定义的时候是protected权限,所以在子类中也是protected权限
    boy.c = 100;  // 不能用,虽然在父类中定义时是public权限,但是继承的方式是protected,所以就被改成了protected
    return 0;
}


// 以private方式继承
#include <iostream>
using namespace std;
class A {
private:
    int a;
protected:
    int b;
public:
    int c;
};
class B : private A {
public:
    void test() {
        this->a = 100;  // 不能用,因为 a是私有权限,不能被继承
        this->b = 100;  // 可以用,因为 虽然b是受保护的权限,但是依然可以被继承
        this->c = 100;  // 可以用,但此时被修改成了private权限
    }
};
int main() {
    class B boy;
    boy.test();
    boy.a = 100;  // 不能用,它在父类中定义是用的private权限,都没有被继承,肯定也就用不了
    boy.b = 100;  // 不能用,虽然在父类中定义时是protected权限,但是继承的方式是private,所以就被改成了private
    boy.c = 100;  // 不能用,虽然在父类中定义时是public权限,但是继承的方式是private,所以就被改成了private
    return 0;
}



总之,继承方式不同,成员权限被修改。

image-20230701115041473

重写

子类定义了与父类相同名字的函数,覆盖了父类的这个函数。常用于扩展或者重新编写功能。

被重写的父类成员函数,无论是否有重载,子类中都不会继承

override

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

override 并不会影响程序的执行结果,仅仅是作用于编译阶段,用于检查子类是否真的重写父类函数

多继承

一个子类可以同时继承多个父类

如果多个父类有相同的函数名,则要在子类中重写这个函数,否则会出现编译错误,原因是二义性。

多态

多种状态(形态)。简单来说,我们可以将多态定义为消息以多种形式显示的能力。
多态是以封装和继承为基础的。

有多个相同名字的函数,当调用时,会根据调用时的方式不同,调用不同的函数。当调用函数时,到底用哪个要根据调用时的参数而确定。

静态多态

#include <iostream>
using namespace std;
int Add(int a, int b) {
    cout << "int类型的函数被调用\n";
    return a + b;
}
double Add(double a, double b) {
    cout << "double类型的函数被调用\n";
    return a + b;
}
int main() {
    Add(10, 20);
    Add(10.0, 20.0);
    return 0;
}

上述的Add函数,在编译阶段就已经确定了,因为编译器会根据实参的类型自动确定调用哪个函数,所以叫做静态多态。

动态多态

#include<iostream>

using namespace std;

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

class Children : public Father {
public:
    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;
}

输出:

father show
children  show
father show

怎样让父类型定义的指针变量调用子类中的show函数呢?我们可以通过virtual定义一个虚函数。
通过virtual可以实现真正的多态。
虚函数可以在父类的指针指向子类对象的前提下,通过父类的指针调用子类的成员函数。
这种技术让父类的指针或引用具备了多种形态,这就是所谓的多态。

最终形成的功能:
如果父类指针指向的是一个父类对象,则调用父类的函数。
如果父类指针指向的是一个子类对象,则调用子类的函数。

注意:
在父类的函数上添加 virtual 关键字,可使子类的同名函数也变成虚函数。

C++ 多态分类及实现:

  • 重载多态(Ad-hoc Polymorphism,编译期):函数重载、运算符重载
  • 子类型多态(Subtype Polymorphism,运行期):虚函数
  • 参数多态性(Parametric Polymorphism,编译期):类模板、函数模板
  • 强制多态(Coercion Polymorphism,编译期/运行期):基本类型转换、自定义类型转换

函数

内联函数

inline

函数的一个主要缺点是每次调用函数时,都会产生一定的性能开销。这是因为CPU必须存储它正在执行的当前指令的地址(这样它才知道之后该从哪里返回)以及其他寄存器,所有函数参数必须被创建并赋值,并且程序必须跳转到新的位置执行。因此就地(in-place)编写的代码要快得多。

C++提供了一种将函数的优点与就地编写的代码速度相结合的方法:内联函数(inline function)。inline 关键字用于请求编译器把你的函数作为内联函数。当编译器编译你的代码时,所有内联函数都会就地展开 -- 也就是说,函数调用被替换为函数本身内容的副本,这会删除函数调用开销!缺点是因为内联函数在每次函数调用时就地扩展,这可能会使编译的代码变得更大,特别是如果内联函数很长或对内联函数有很多调用。

特点:

1.内联函数是C++语言提供的一种特性,可以在函数定义时使用inline关键字进行声明。

2.内联函数是由编译器实现的,因此内联函数的调用是有类型检查的

3.内联函数的代码是由编译器直接嵌入到调用该函数的地方,因此内联函数的代码是可以进行调试的,可以在代码中打断点进行调试。

4.内联函数的参数和返回值都是有类型的,并且可以使用函数的所有特性(如const和引用参数)。

5.内联函数的使用受到编译器的限制,只有当函数体比较小、被频繁调用、参数是常量等条件满足时才会进行内联。

函数重载

重载是指不同的函数使用相同的函数名,但是函数的参数个数和类型可以不同,调用时根据函数的参数来调用最匹配的。

重载是发生在一个类内的对于同名函数的不同实现机制。

函数重写

重写(也叫覆盖)是指在派生类中重新对基类中的虚函数重新进行功能实现(函数名和参数都是一样的,但是可以实现具体的功能)。

重写是发生在父类和子类之间同名同参函数的改写。

函数模板

函数模板代表着一组函数,只是该组函数的参数类型和返回值类型可以变化而已。

在C++程序中定义一个函数模板时,只是简单地在上述那种具有类型参数T的函数之前增加“template”,以表示下面要定义一个模板,它具有的可变化类型参数名称叫做T(表示某类型的标识符)。例如:

template<class T>T max(T a[], int n) {
	T max;
	for (int i = 1; i < n; i++) {
		if (max < a[i])max = a[i];
	}
	return max;
}

虚析构函数

虚函数

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

最终形成的功能:
如果父类指针指向的是一个父类对象,则调用父类的函数。
如果父类指针指向的是一个子类对象,则调用子类的函数。

函数前添加virtual关键字让函数成为虚函数

注意:

1.只有类的成员函数才能说明为虚函数;
2.静态成员函数不能是虚函数;
3.内联函数不能为虚函数;
4.构造函数不能是虚函数;
5.析构函数可以是虚函数,而且通常声明为虚函数

final

一个特殊的关键字,有2个作用

  • 禁止虚函数被重写
  • 禁止类被继承

注意:

  • 只有虚函数才能被标记为final ,其他的普通函数无法标记final

父子指针指向问题

语法上,父类指针可以指向子类,子类通过强制类型转换(不安全)也能指向父类。通过指针操作时,判断指针访问了谁的成员,就是看原型指针的类型是什么,访问的就是该类型的成员。需要特别注意的是:

(1)定义父类类型的指针p,指向子类类型(地址值为子类类型指针):这种做法是安全的,因为指针p是父类类型,只能访问父类中定义的成员,而子类继承了父类所有的成员,所以指针p不会出现非法访问。如果此时,p想访问子类特有函数,在语法上不被允许,因为p是父类类型指针,但可以在父类中声明虚函数,然后在子类重写,那么此时p在调用虚函数时,即可访问到子类重写的函数。

(2)定义子类类型的指针t,指向父类类型(地址值为父类类型指针):这种做法语法上不允许,但能通过强制类型转换(非常不安全)让语法正确。因为t是子类类型指针,能访问子类定义的所有成员,但其原型却是一个父类指针,若访问的刚好是父类成员,可能不会有问题,倘若访问的是子类特有成员,则会出现非法访问,这种指向是非常不推荐的。

class Base 
{
    void BaseFunc() {}
    virtual void ChildSpecial() {}
};
class Child : public Base
{
    void ChildFunc() {}
    void ChildSpecial() override { std::cout << "Call Child Special Func."; }
};

Child* pChild = new Child();
// 安全,pBase只能访问Base内定义的成员,而pChild继承了Base的所有成员,故不会出现非法访问
Base* pBase = pChild;
// 想用pBase访问Child特有函数,可在Base中定义虚函数,然后在子类重写,即实现了父类指针调用子类特有函数
pBase->ChildSpecial(); // 打印 Call Child Special Func.

Base* tBase = new Base();
Child* tChild = tBase; // 语法错误,不允许的转化
// 语法上允许,但不安全,tChild访问的是Base中不存在的函数时,会出现非法访问,不推荐这么做
Child* tChild = static_cast<Child*>(tBase);

纯虚函数

一个可以只写函数声明,不写函数具体过程的函数

可以将虚函数声明为纯虚函数,语法格式为:

class XXX{
    virtual 返回值类型 函数名 (函数参数) = 0;
};

纯虚函数没有函数体,只有函数声明,在虚函数声明的结尾加上=0,表明此函数为纯虚函数。

友元函数

friend

一句话:一种可以访问private权限的成员 的 方法

私有成员只能在类的成员函数内部访问,如果想在别处访问对象的私有成员,只能通过类提供的接口(成员函数)间接地进行,这固然能够带来数据隐藏的好处,利于将来程序的扩充,但也会增加程序书写的麻烦

友元可以看成是现实生活中的 好闺蜜 或者是 好基友

友元函数可以直接访问类中的私有成员

转换函数

所谓转换函数指的是类型之间的转换,比如把自定义的类型转换成内建类型(比如double),后者向相反的方向转。

operator typeName();

比如:
operator double()const {
        return (double)(m_numerator / m_denominator);
    }

内存管理

malloc、calloc、realloc、alloca

  1. malloc:申请指定字节数的内存。申请到的内存中的初始值不确定。
  2. calloc:为指定长度的对象,分配能容纳其指定个数的内存。申请到的内存的每一位(bit)都初始化为 0。
  3. realloc:更改以前分配的内存长度(增加或减少)。当增加长度时,可能需将以前分配区的内容移到另一个足够大的区域,而新增区域内的初始值则不确定。
  4. alloca:在栈上申请内存。程序在出栈的时候,会自动释放内存。但是需要注意的是,alloca 不具可移植性, 而且在没有传统堆栈的机器上很难实现。alloca 不宜使用在必须广泛移植的程序中。C99 中支持变长数组 (VLA),可以用来替代 alloca。

malloc、free

用于分配、释放内存

malloc、free 使用

申请内存,确认是否申请成功

char *str = (char*) malloc(100);
assert(str != nullptr);

释放内存后指针置空

free(p); 
p = nullptr;

new、delete

  1. new / new[]:完成两件事,先底层调用 malloc 分配了内存,然后调用构造函数(创建对象)。
  2. delete/delete[]:也完成两件事,先调用析构函数(清理资源),然后底层调用 free 释放空间。
  3. new 在申请内存时会自动计算所需字节数,而 malloc 则需我们自己输入申请内存空间的字节数。

new、delete 使用

申请内存,确认是否申请成功

int main()
{
    T* t = new T();     // 先内存分配 ,再构造函数
    delete t;           // 先析构函数,再内存释放
    return 0;
}

new:C中可以使用malloc分配内存,而在C++中可以使用new分配内存。
new会根据类型来确定需要多少字节的内存(没有名字),然后找到这样的内存,并返回地址。
delete:类似于free

定位 new

定位 new(placement new)允许我们向 new 传递额外的地址参数,从而在预先指定的内存区域创建对象。

new (place_address) type
new (place_address) type (initializers)
new (place_address) type [size]
new (place_address) type [size] { braced initializer list }
  • place_address 是个指针
  • initializers 提供一个(可能为空的)以逗号分隔的初始值列表

delete this 合法吗?

Is it legal (and moral) for a member function to say delete this?

合法,但:

  1. 必须保证 this 对象是通过 new(不是 new[]、不是 placement new、不是栈上、不是全局、不是其他对象成员)分配的
  2. 必须保证调用 delete this 的成员函数是最后一个调用 this 的成员函数
  3. 必须保证成员函数的 delete this 后面没有调用 this 了
  4. 必须保证 delete this 后没有人使用了

动态数组

//分配
int * psome = new int [10];
//释放,应该释放整个数组,而不是指针指向的元素
delete [] psome;

泛型编程

泛型编程是一种编程范式。

它允许在编写代码时使用泛化的数据类型和算法,以增加代码的可重用性和灵活性。

在 C++ 中,泛型编程主要通过模板(Templates)来实现

泛型编程的核心思想是编写与特定数据类型无关的代码,使代码能够适用于多种数据类型而不需要重复编写

通过使用泛型,可以编写通用的数据结构和算法,使其适用于不同的数据类型,从而提高代码的复用性和扩展性。

模板是一种将类型参数化的工具,可以用来定义通用的类、函数和算法

通过使用模板,可以编写可以适用于多种数据类型的代码

C++ 提供了类模板(Class Templates)和函数模板(Function Templates)来支持泛型编程。

类模板

类模板时类的抽象,类模板本身不是类,而是用来生成类的“配方”,一个类模板说明可以定义出具有共性(除类型参数或普通参数外,其余全相同)的一组类。

类的实例化——对象;类模板的实例化——类。

template<class T,int i>
class TestClass{
        // 类成员和成员函数定义
        ……
}

int main(){
    class TestClass<int> intArray(5);
    class TestClass<double> doubleArray(10);
}

其中,template <typename T> 表示这是一个类模板,并使用 T 作为类型参数。T 可以是任意合法的标识符,用于表示通用的类型.

函数模板

在 C++ 中,一个函数模板可以对应多个函数。当使用模板生成代码时,编译器会根据传递的参数类型实例化对应的函数

函数模板本身并不是一个具体的函数定义,而是一个通用的模板,用于生成特定类型的函数定义。当编译器在代码中遇到对函数模板的调用时,它会根据传递的参数类型生成具体的函数定义

#include <iostream>

template <typename T>
void printValue(T value) {
    std::cout << "Value: " << value << std::endl;
}

int main() {
    printValue(10);      // 调用 printValue<int>(int)
    printValue(3.14);    // 调用 printValue<double>(double)

    return 0;
}

容器

定义

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

用途

容器的用途包括但不限于以下几个方面:

  1. 存储和组织数据:容器可以存储一组相关的数据,并提供了适当的数据结构来组织这些数据。例如,数组容器可以存储一系列具有相同类型的元素,并提供通过索引访问元素的能力。
  2. 动态内存管理:一些容器(如std::vectorstd::list)具有动态调整大小的能力,可以根据需要动态分配和释放内存,使得在运行时可以灵活地管理数据的大小。
  3. 提供数据访问和操作接口:容器提供了丰富的方法和操作符来访问和操作容器中的数据。通过这些接口,可以方便地对数据进行插入、删除、查找、排序等操作。
  4. 简化代码实现:使用容器可以简化代码的编写和维护。容器提供了封装好的数据结构和操作,避免了手动实现复杂的数据结构和算法,使代码更加简洁和可读性更高。
  5. 提供算法支持:C++标准库还提供了许多与容器一起使用的算法,如排序、查找、遍历等,这些算法可以方便地应用于容器中的数据。

通过选择合适的容器,可以根据具体的需求和场景来存储和管理数据,提高代码的效率和可维护性。不同的容器具有不同的特点和适用性,因此在选择容器时应根据数据访问、插入删除的需求以及空间和时间复杂度的考虑做出合理的选择。

string容器

std::string 是 C++ 标准库中提供的字符串容器,它用于存储和操作字符串。std::string 提供了许多字符串操作的方法,使得在 C++ 中处理字符串变得更加方便和高效。

//定义
String str1 = "Hello World";//直接赋值
String str2 = new String("Hello World");//通过构造方法创建对象

using namespace std;
std::string str3; // 创建一个空的 std::string 对象

//操作
System.out.println(str1.equals(str2));//比较字符串是否相等
str1 += str2;//str2拼接到str1尾部

vector容器

std::vector 是 C++ 标准库中提供的动态数组容器,它能够存储和管理任意类型的元素。std::vector 提供了方便的方法来操作数组,使得在 C++ 中处理动态数组变得更加灵活和高效。

它是一种动态数组,在运行阶段设置vector对象的长度。它会自动new和delete。该容器前端封闭。

//定义
vector<typename> vt(n_elem);//n_elem可以说整型常量或变量
vector(vt.begin(), vt.end());//vt拷贝

//操作
vt.push_back(a);//尾部插入元素
vt.pop_back(a);//尾部删除元素
vt.begin();//第一个元素
vt.end();//最后一个元素的后一位
vt.assign(v1.begin(), v1.end()); //将[beg, end)区间中的数据拷贝赋值给vt。

其他容器

1.std::array,它是一种静态数组。

array<typename, n_elem> arr;//n_elem不能是变量

2.std::set 有序集合,存储唯一值,支持快速的插入、查找和删除操作。

3.std::map 关联数组(字典)(无序集合),按键值对存储和访问数据。

4.std::stack 栈,遵循后进先出(LIFO)的原则,只能在栈顶进行插入和删除操作。

5.std::queue 队列,遵循先进先出(FIFO)的原则,只能在队尾进行插入,在队头进行删除操作。

lambda表达式

定义

在C++中,Lambda表达式是一种用于创建匿名函数的便捷语法。Lambda表达式允许我们在需要函数对象的地方定义和使用简短的函数,而无需显式定义具名函数。

lambda表达式其实就是一个内联函数。

简单来说就是一些实现一些简单的功能不需要新建一个有名的函数,直接可以使用lambda完成。

语法

在编写lambda表达式的时候,可以忽略参数列表和返回值类型,但是必须包含前后的捕获列表和函数体, 捕获列表的中括号不能省略,编译根据它来识别后面是否是lambda表达式 ,并且它还有一个作用是能够让lambda的函数体访问它所处作用域的成员。

//语法 
[捕获列表](参数列表)->返回值类型{函数体}

[](int a ,int b)->int{return a + b;} ; //一个简单的加法

捕获列表

labmda表达式需要在函数体中定义,这时如果想访问所处函数中的某个成员,那么就需要使用捕获列表了。捕获列表的写法通常有以下几种形式:

形式 作用
[a] 表示值传递方式捕获变量 a
[=] 表示值传递方式捕获所有父作用域的变量(包括this)
[&a] 表示引用方式传递捕获变量a
[&] 表示引用传递方式捕获所有父作用域的变量(包括this)
[this] 表示值传递方式捕获当前的this指针
[=,&a,&b] 引用方式捕获 a 和 b , 值传递方式捕获其他所有变量 (这是组合写法)

用法

用法分两种:第一种是建立表达式(类似函数),需要手动调用;第二种是不建立表达式,直接当成语句。

lambda表达式 定义出来并不会自己调用,需要手动调用。 如下面所示的,一个加法的案例

  • 使用变量接收,然后再调用
#include <iostream>
using namespace std;

int main(){
    //1. 接收lambda表达式,然后调用
    auto f = [](int a ,int b)->int{return a + b ;}; 

    //2. 调用lambda表达式
    int result = f(3,4); //调用lambda函数,传递参数
    
    cout << "result = " << result << endl;
    return 0 ;
}
  • 不接受表达式,直接调用
#include <iostream>
using namespace std;

int main(){
    ///2. 不接收,立即调用。 后面的小括号等同于调用这个函数。
    int result= [](int a ,int b){return a + b }(3,4); 
 
    cout << "result = " << result << endl;    
    return 0 ;
}

应用

编写lamdda表达式很简单,但是用得上lambda表达式的地方比较特殊。一般会使用它来封装一些逻辑代码,使其不仅具有函数的包装性,也具有可见的自说明性。

在C++ 中,函数的内部不允许再定义函数,如果函数中需要使用到某一个函数帮助计算并返回结果,代码又不是很多,那么lambda表达式不失为一种上佳选择。如果没有lambda表达式,那么必须在外部定义一个内联函数。 来回查看代码稍显拖沓,定义lambda函数,距离近些,编码效率高些。

相当于内联函数。

异常处理

定义

在计算机程序中,异常是指程序执行过程中发生的错误或异常情况。这些异常可能是由于无效的输入、资源不可用、逻辑错误等原因引起的。通过使用异常处理机制,程序可以在出现异常时采取相应的措施,如错误处理、资源释放、日志记录等。

异常处理规范

异常处理遵循以下基本原则:

  1. 抛出异常(Throwing Exceptions):当出现错误或异常情况时,可以使用 throw 关键字抛出异常。抛出的异常可以是内置类型(如整数、字符等)或自定义类型。

    throw MyException("Something went wrong");复制Error复制成功...

  2. 捕获异常(Catching Exceptions):使用 try-catch 块来捕获和处理异常。try 块用于包裹可能引发异常的代码块,而 catch 块用于捕获并处理特定类型的异常。

    try {
        // 可能引发异常的代码块
    } catch (ExceptionType ex) {
        // 处理 ExceptionType 类型的异常
    }
    
  3. 处理异常(Handling Exceptions):在 catch 块中,可以对捕获的异常进行处理。处理异常的方式可以是输出错误信息、进行日志记录、恢复程序状态等。也可以选择重新抛出异常或抛出其他异常。

    try {
        // 可能引发异常的代码块
    } catch (ExceptionType1 ex1) {
        // 处理 ExceptionType1 类型的异常
    } catch (ExceptionType2 ex2) {
        // 处理 ExceptionType2 类型的异常
    } catch (...) {
        // 处理其他类型的异常
    }
    
  4. 栈展开(Stack Unwinding):当发生异常时,程序会在调用栈中寻找匹配的 catch 块来处理异常。如果没有找到匹配的 catch 块,异常会被传递给上层调用栈中的调用者,直到找到匹配的 catch 块为止。如果最终没有找到任何匹配的 catch 块,程序将终止,并显示未捕获的异常信息。

  5. 异常安全(Exception Safety):在编写代码时,需要考虑异常安全性。这意味着在出现异常时,程序应保持稳定状态,并确保资源的正确释放,以避免资源泄漏和数据损坏

命名空间

定义

在 C++ 中,命名空间是一种用于将代码实体(如变量、函数、类等)组织在一起的机制。它提供了一个独立的、隔离的命名区域,以防止命名冲突,并提供更好的代码模块化和可读性。

命名空间的定义采用以下语法:

namespace namespace_name {
    // 命名空间内的声明和定义
}

其中,namespace_name 是命名空间的名称,可以是任意有效的标识符。命名空间的定义可以出现在全局作用域中,也可以嵌套在其他命名空间内。

用法

使用命名空间可以通过两种方式:

  1. 前向声明和限定命名空间中的实体

    // 前向声明命名空间
    using namespace namespace_name;
    
    // 限定命名空间中的实体
    namespace_name::entity_name;
    

    使用 using namespace 声明可以将整个命名空间引入到当前作用域中,使其中的实体可以直接使用,而无需在名称前添加命名空间的限定符。

    使用 namespace_name::entity_name 语法可以限定命名空间中的特定实体,以便只使用该实体而不引入整个命名空间。

  2. 完全限定命名空间中的实体

    namespace_name::entity_name;
    

    使用完全限定命名空间和实体名称的语法可以直接指定要使用的实体,而不需要引入整个命名空间

嵌套

命名空间可以嵌套在其他命名空间内部,形成命名空间的层次结构。这种嵌套结构可以帮助更好地组织和管理代码,使命名空间的层次结构更具可读性。

namespace outer_namespace {
    // outer_namespace 中的声明和定义

    namespace inner_namespace {
        // inner_namespace 中的声明和定义
    }
}

在嵌套的命名空间中,内部命名空间中的实体可以直接访问外部命名空间中的实体,而无需限定命名空间。、

匿名命名空间

匿名命名空间是一种特殊的命名空间,其名称为空。在匿名命名空间中定义的实体只在当前文件内可见,不会对其他文件造成影响。它可以用于定义局部于文件的全局变量、函数或类,以避免对其他文件的全局作用域造成命名冲突。

namespace {
    // 匿名命名空间中的声明和定义
}

重命名

通过使用 namespace 关键字,可以将命名空间重命名为更短或更具可读性的名称。

namespace short_name = long_namespace_name;

在重命名后,可以使用 short_name 作为原本较长命名空间的别名,以简化代码并提高可读性。

智能指针

产生原因

c++ 把内存的控制权对程序员开放,让程序显式的控制内存,这样能够快速的定位到占用的内存,完成释放的工作。但是此举经常会引发一些问题,比如忘记释放内存。由于内存没有得到及时的回收、重复利用,所以在一些c++程序中,常会遇到程序突然退出、占用内存越来越多,最后不得不选择重启来恢复。造成这些现象的原因可以归纳为下面几种情况:

1.野指针

出现野指针的有几个地方 :

  • 指针声明而未初始化,此时指针的将会随机指向
  • 内存已经被释放、但是指针仍然指向它。这时内存有可能被系统重新分配给程序使用,从而会导致无法估计的错误

2.重复释放

程序试图释放已经释放过的内存,或者释放已经被重新分配过的内存,就会导致重复释放错误.

3.内存泄漏

不再使用的内存,并没有释放,或者忘记释放,导致内存没有得到回收利用。 忘记调用delete。

为了解决普通指针的隐患问题,c++在98版本开始追加了智能指针的概念,并在后续的11版本中得到了提升

c++11标准用 unique_ptr | shared_ptr | weak_ptr 等指针来自动回收堆中分配的内存。智能指针的用法和原始指针用法一样,只是它多了些释放回收的机制罢了。

C++ 标准库(STL)

头文件:#include <memory>

C++ 98

std::auto_ptr<std::string> ps (new std::string(str));

C++ 11

  1. shared_ptr
  2. unique_ptr
  3. weak_ptr
  4. auto_ptr(被 C++11 弃用)
  • Class shared_ptr 实现共享式拥有(shared ownership)概念。多个智能指针指向相同对象,该对象和其相关资源会在 “最后一个 reference 被销毁” 时被释放。为了在结构较复杂的情景中执行上述工作,标准库提供 weak_ptr、bad_weak_ptr 和 enable_shared_from_this 等辅助类。
  • Class unique_ptr 实现独占式拥有(exclusive ownership)或严格拥有(strict ownership)概念,保证同一时间内只有一个智能指针可以指向该对象。你可以移交拥有权。它对于避免内存泄漏(resource leak)——如 new 后忘记 delete ——特别有用。

shared_ptr

多个智能指针可以共享同一个对象,对象的最末一个拥有着有责任销毁对象,并清理与该对象相关的所有资源。

  • 支持定制型删除器(custom deleter),可防范 Cross-DLL 问题(对象在动态链接库(DLL)中被 new 创建,却在另一个 DLL 内被 delete 销毁)、自动解除互斥锁

允许多个智能指针共享同一块内存,由于并不是唯一指针,所以为了保证最后的释放回收,采用了计数处理,每一次的指向计数 + 1 , 每一次的reset会导致计数 -1 ,直到最终为0 ,内存才会最终被释放掉。 可以使用use_cout 来查看目前的指针个数。

unique_ptr

unique_ptr 是 C++11 才开始提供的类型,是一种在异常时可以帮助避免资源泄漏的智能指针。采用独占式拥有,意味着可以确保一个对象和其相应的资源同一时间只被一个 pointer 拥有。一旦拥有着被销毁或编程 empty,或开始拥有另一个对象,先前拥有的那个对象就会被销毁,其任何相应资源亦会被释放。

  • unique_ptr 用于取代 auto_ptr

也就是只有这个指针能够访问这片空间,不允许拷贝,但是允许移动(转让所有权)

weak_ptr

weak_ptr 允许你共享但不拥有某对象,一旦最末一个拥有该对象的智能指针失去了所有权,任何 weak_ptr 都会自动成空(empty)。因此,在 default 和 copy 构造函数之外,weak_ptr 只提供 “接受一个 shared_ptr” 的构造函数。

  • 可打破环状引用(cycles of references,两个其实已经没有被使用的对象彼此互指,使之看似还在 “被使用” 的状态)的问题

对于引用计数法实现的计数,总是避免不了循环引用(或环形引用)的问题,即我中有你,你中有我,shared_ptr也不例外。 下面的例子就是,这是因为f和s内部的智能指针互相指向了对方,导致自己的引用计数一直为1,所以没有进行析构,这就造成了内存泄漏。

#include <iostream>
#include <memory>

using namespace std;

class Son;

class Father {
private:
    shared_ptr<Son> son;
public:
    Father() {
        cout << "father 构造" << endl;
    }

    ~Father() {
        cout << "father 析构" << endl;
    }

    void setSon(shared_ptr<Son> son) {
        this->son = son;
    }
};

class Son {
private:
    shared_ptr<Father> father;
public:
    Son() {
        cout << "son 构造" << endl;
    }

    ~Son() {
        cout << "son 析构" << endl;
    }

    void setFather(shared_ptr<Father> father) {
        this->father = father;
    }
};


int main() {
    shared_ptr<Father> f(new Father());
    shared_ptr<Son> s(new Son());
    f->setSon(s);
    s->setFather(f);
}

解决办法:

定义对象的时候,用shared_ptr指针;引用对象的地方,用weak_ptr指针。

#include <iostream>
#include <memory>

using namespace std;

class Son;

class Father {
private:
    weak_ptr<Son> son;
public:
    Father() {
        cout << "father 构造" << endl;
    }

    ~Father() {
        cout << "father 析构" << endl;
    }

    void setSon(shared_ptr<Son> son) {
        this->son = son;
    }
};

class Son {
private:
    weak_ptr<Father> father;
public:
    Son() {
        cout << "son 构造" << endl;
    }

    ~Son() {
        cout << "son 析构" << endl;
    }

    void setFather(shared_ptr<Father> father) {
        this->father = father;
    }
};


int main() {
    shared_ptr<Father> f(new Father());
    shared_ptr<Son> s(new Son());
    f->setSon(s);
    s->setFather(f);
}

auto_ptr

被 c++11 弃用,原因是缺乏语言特性如 “针对构造和赋值” 的 std::move 语义,以及其他瑕疵。

auto_ptr 与 unique_ptr 比较

  • auto_ptr 可以赋值拷贝,复制拷贝后所有权转移;unqiue_ptr 无拷贝赋值语义,但实现了move 语义;
  • auto_ptr 对象不能管理数组(析构调用 delete),unique_ptr 可以管理数组(析构调用 delete[] );

I/O操作

基本输出

输出布尔数据

在c/c++中,在对bool类型的数据做输出的时候,打印的是 0 、1 ,如果希望看到的是 true 和 false ,那么可以使用 boolalpha 操作符

#include <iostream>

using namespace std;

int main() {

    bool flag = false;
    cout << "flag的值是:" << flag << endl; // 打印 0

    //操作符只会影响后续的输出  打印 0  false
    cout << "flag的值是:" << flag << " 添加操作符后:" << boolalpha << flag << endl;

    return 0;
}

输出整型数字

在输出数字时,可以选择使用十进制八进制十六进制 输出 ,它们只会影响整形数字, 默认会采用十进制输出数字

#include <iostream>

using namespace std;

int main() {

    cout << "十进制:" << dec << 9 << endl;  // 9
    cout << "八进制:" << oct << 9 << endl;  // 10
    cout << "十六进制:" << hex << 10 << endl; // a

    // 若想在输出前面表示打印的数值显示前缀进制标识,可以使用showbase关键字
    cout << showbase;
    // 默认即采用十进制输出
    cout << "十进制:" << dec << 9 << endl;  // 9
    cout << "八进制:" << oct << 9 << endl;   // 011  前面是数字0,不是字母O
    cout << "十六进制:" << hex << 10 << endl;  // 0xa
    cout << noshowbase;

    return 0;
}

输出浮点数

c++ 对浮点数的输出默认只会输出六位 ,那么在应对较多浮点数的时候,则常常会丢失精度,导致后面的小数位无法输出。标准库也提供了针对浮点数的输出格式cout.precision() | setprecision(),允许指定以多高的精度输出浮点数

#include<iostream>

using namespace std;

int main() {

    double a = 10.12345678901234;
    cout.precision(3); //  设置输出多少位数字 ,该函数有返回值,为设置的精度值。
    cout << "a=" << a << endl;  // 10.1
    
    return 0;
}

小数点后面都是0,默认是不会被输出的,若想强制输出,可以使用showpoint关键字,配合precision 可以精确打印

#include<iostream>

using namespace std;

int main() {

    float f = 10.12;
    cout.precision(6);
    cout << showpoint << "f=" << f << noshowpoint << endl;  // 10.00

    return 0;
}

基本输入

读取基本类型输入

#include <iostream>

int main() {
    int number;
    std::cout << "Enter a number: ";
    std::cin >> number;
    std::cout << "You entered: " << number << std::endl;

    return 0;
}复制Error复制成功...

在这个示例中,我们使用 cin 从用户输入中读取一个整数,并将其存储在 number 变量中。然后,我们使用 cout 将该值输出到控制台。

多个输入

#include <iostream>

int main() {
    int num1, num2;
    std::cout << "Enter two numbers: ";
    std::cin >> num1 >> num2;
    std::cout << "Sum: " << (num1 + num2) << std::endl;

    return 0;
}复制Error复制成功...

在这个示例中,我们使用 cin 连续读取两个整数,并将它们存储在 num1num2 变量中。然后,我们计算它们的和,并使用 cout 将结果输出到控制台。

需要注意的是,cin 默认以空格或换行符作为输入的分隔符。输入的数据类型必须与接收数据的变量类型匹配,否则可能导致不正确的结果或输入错误。

此外,还要考虑输入错误的处理,例如输入不合法的值或输入结束时的处理。可以使用 cin 的错误状态和流控制来进行错误处理。

文件操作

C++中处理文件操作的有三个主要对象 istreamostreamfstream 。 需要添加头文件 #include<fstream>

  • 文件操作常用类

到底是输入还是输出,是站在程序的角度看就ok.

数据类型 描述
ofstream 表示输出文件流,用于创建文件并向文件写入信息。
ifstream 表示输入文件流,用于从文件读取信息。
fstream 示文件流,且同时具有 ofstream 和 ifstream 两种功能,这意味着它可以创建文件,向文件写入信息,也可以从文件读取信息。
  • 文件操作模式
模式标志 描述
ios::app 追加模式。所有写入都追加到文件末尾。
ios::ate 文件打开后定位到文件末尾。
ios::in 打开文件用于读取。
ios::out 打开文件用于写入。
ios::trunc 如果该文件已经存在,其内容将在打开文件之前被截断,即把文件长度设为 0。

读取文件

文件仅仅是一些简单的字符串

#include <iostream>
#include <string>
#include <fstream>

using namespace std;

int main() {
    // 打开文件
    fstream file("./main.cpp", ios::in);
    // 文件是否能正常打开
    if (file.is_open()) {
        string line;
        // getline 用于读取文件的整行内容,直到碰到文件末尾或者超过字符串可存储的最大值才会返回
        while (getline(file, line)) {
            cout << line << endl;
        }
        // 关闭文件
        file.close();
    } else {
        cout << "文件无法正常打开! " << endl;
    }
    return 0;
}

写入文件

实际上和读取文差不多,如果能从文件中读取内容,那么往文件中写入内容也没有多大难处。

#include <iostream>
#include <fstream>

using namespace std;

int main() {
    // 若问写入操作,文件不存在,会自动创建文件
    // out: 每次写入都会覆盖源文件内容
    // app(append追加) : 在末尾处追加内容
    fstream file{"test.txt", ios::app};
    if (file.is_open()) {
        cout << "正常打开文件" << endl;
        // 写入数据
        file << "hi c++";
        // 写入换行
        file << "\n\n";  // 表示换2个行
        // 完成,且关闭文件
        file.close();
    } else {
        cout << "无法正常打开文件" << endl;
    }
    return 0;
}

疑问

虚函数和纯虚函数的区别?

1.虚函数:

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

函数前添加virtual关键字让函数成为虚函数。

函数前也可添加final关键字(虚函数和类的专属关键字)让虚函数或类:

  • 禁止虚函数被重写,该函数是最终函数
  • 禁止类被继承,该类是最终类

2.纯虚函数:

一个可以只写函数声明,不写函数具体过程的函数

纯虚函数没有函数体,只有函数声明,在虚函数声明的结尾加上=0,表明此函数为纯虚函数。

抽象类是什么?有什么作用?

包含纯虚函数的类称为抽象类(Abstract Class)

之所以说它抽象,是因为它无法实例化,也就是无法创建对象,原因:纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间。

抽象类通常是作为基类,让派生类去实现纯虚函数派生类必须实现纯虚函数才能被实例化

多态?重载?

多态:类中有相同名字的函数。重点是有很多个同名的成员函数,它们的调用方式有很多种。调用方式不同,调用的函数不同(调用的成员函数可以有多种形态)

重载:类中成员函数名字相同但,参数不同。重点是同一个类中,同名函数的参数不同。

静态多态和动态多态?

根据参数的不同,调用成员函数有多种形态->静态多态

添加虚函数,让父类的指针或引用也具备了多种形态->动态多态

posted @ 2023-09-04 16:43  我好想睡觉啊  阅读(150)  评论(0编辑  收藏  举报