【C++程序设计】知识点汇总(常用知识点基本都有)
  W7xauqsNtuHG 2023年11月02日 113 0

基本概念

初学者容易混淆的概念:

MinGW、MSC、VC6和C++17是与C++编程相关的概念,它们具有以下区别:

MinGW(Minimalist GNU for Windows)是一个用于Windows平台的开发环境,它提供了一套用于编译和链接C和C++程序的工具链。MinGW使用GNU工具集(如GCC编译器)来编译和构建程序。

MSC(Microsoft C/C++)是微软公司提供的C和C++编译器。它是Visual Studio集成开发环境(IDE)的一部分,用于在Windows平台上编译和构建程序。

VC6(Visual C++ 6.0)是微软发布的Visual C++系列的一个版本,它是Visual Studio 6.0的一部分。VC6是一个老版本的C++编译器,发布于1998年。它支持的C++标准是C++98,缺乏对C++11及以后标准的支持。

C++17是C++语言的一个标准,它是C++14的后续标准。C++17引入了一些新的语言特性和库,并对现有的特性进行了改进。C++17提供了更好的模板元编程支持、简化了一些常见的编码模式、增加了一些新的标准库组件等。C++17的发布使得开发者能够使用更多的现代C++特性来编写更简洁、高效、安全的代码。

总结起来,MinGW是用于Windows平台的开发环境,MSC是微软的C/C++编译器,VC6是微软的一个老版本的Visual C++,而C++17是C++语言的一个标准。它们在功能、支持的标准和使用环境上都有所不同。


数据类型

数组

在C++中,使用array[-1]表示对数组中的前一个元素进行访问。然而,这是一种不确定行为,由于C++语言的内存布局和指针算术的规则,它可能会导致未定义的行为。

数组在内存中是连续存储的,每个元素都占用一定的字节。如果你尝试访问array[-1],它会计算出相对于数组首地址的偏移量为-1,并尝试访问该位置的内存。然而,该位置并不属于数组的有效索引范围内,因此结果是未定义的。

以下是一个示例:

int array[5] = {1, 2, 3, 4, 5};
int value = array[-1];  // 未定义的行为

// 输出未定义的值
std::cout << value << std::endl;

在这个示例中,我们尝试通过array[-1]访问数组array中的前一个元素,但这不是有效的操作。由于未定义行为,结果是无法预测的,可能会导致程序崩溃、产生垃圾值或其他意外结果。

因此,在使用数组时,请仅使用有效的索引范围(从0到数组长度减1),以避免未定义行为和潜在的错误。

示例:

#include <iostream>
using namespace std;
int main(){
    int a[5] = {1,3,4,5,6};
    cout << "a[0]" << a[0] << endl;
    cout << "a[4]" << a[4] << endl;
    cout << "a[5]" << a[5] << endl;
    cout << "a[-1]" << a[-1] << endl;
    cout << "a[-2]" << a[-2] << endl;
    return 0;
}

输出结果:

a[0]1
a[4]6
a[5]0
a[-1]0
a[-2]4199705

虽然a[-1]、a[-2]不会报错,但是实际上与数组本身已经没什么关系了。

不同方式定义数组的区别

#include <iostream>
using namespace std;

int main(){
    int* a = new int[3];
    a[0] = 1;
    a[1] = 2;
    a[2] = 3;
    a[3] = 4;
    a[4] = 5;
    cout << a[100] << endl;     //不报错但是是危险用法;会造成不可预知的后果。
    cout << typeid(a).name() << endl;  //Pi;point
    cout << typeid(a[0]).name() << endl; //int
    cout << typeid(a[4]).name() << endl;
    cout << typeid(a[100]).name() << endl;  //int
    cout << endl;

    int b[5];
    cout << b[0] << endl;    //随机的值
    cout << b[4] << endl;    //随机的值
    cout << b[5] << endl;    //随机的值;不属于数组b
    cout << b[100] << endl;    //随机的值;不属于数组b
    cout << typeid(b).name() << endl;  //A5_i
    cout << typeid(b[0]).name() << endl; //int
    cout << typeid(b[4]).name() << endl; //int
    cout << typeid(b[100]).name() << endl;  //int
    cout << endl;

    //int c[5] = {1,2,3,4,5,6};  /越界,语法错误
    int c[5] = {1,2,3,4};
    cout << c[0] << endl;    // 1:指定的值
    cout << c[4] << endl;    // 未指定部分随机的值
    cout << c[5] << endl;    // 随机的值;不属于数组c
    cout << c[100] << endl;    // 随机的值;不属于数组c
    cout << typeid(c).name() << endl;  //A5_i
    cout << typeid(c[0]).name() << endl; //int
    cout << typeid(c[4]).name() << endl; //int
    cout << typeid(c[100]).name() << endl;  //int
    cout << endl;

    int d[] = {1,2,3,4,5,6};
    //int d[];   //错误写法,不完整的定义。
    cout << d[0] << endl;    // 1
    cout << d[5] << endl;    // 6
    cout << d[6] << endl;    // 随机的值;不属于数组d
    cout << d[100] << endl;    // 随机的值;不属于数组d
    cout << typeid(d).name() << endl;    // A6_i
    cout << typeid(d[0]).name() << endl;    //i
    cout << typeid(d[6]).name() << endl;    //i
    cout << typeid(d[100]).name() << endl;    //i

    return 0;
}

输出结果:

-17891602
Pi
i
i
i

8
14424176
0
0
A5_i
i
i
i

1
0
0
0
A5_i
i
i
i

1
6
24
0
A6_i
i
i
i

在 C++ 中,数组越界访问不会受到编译器的直接限制。这是因为 C++ 采用了一种灵活的内存访问方式,给予开发者更多的自由度和控制权。C++中的数组是通过指针和下标运算符来实现的,指针本质上是一个内存地址,可以进行加法和减法运算。

虽然 C++ 不会在编译阶段检查数组访问是否越界,但在运行时会发生未定义的行为。当你越界访问数组时,可能会读取或写入未分配给数组的内存位置,这可能导致程序崩溃、数据损坏或产生不可预测的结果。

为了避免数组越界访问带来的问题,开发者应该始终保证只访问数组有效范围内的索引。可以使用循环结构和条件判断来确保代码的正确性。此外,许多现代的开发工具也提供了静态代码分析和动态调试等功能,能够帮助开发者尽早发现并纠正数组越界访问问题。

求数组元素个数

#include <iostream>
using namespace std;

int main(){
    //int c[5] = {1,2,3,4,5,6};  /越界,语法错误
    int c[5] = {1,2,3,4};
    int len1 = sizeof(c)/sizeof(c[0]);
    cout << len1 << endl;  //5
    cout << endl;

    int d[5] = {1,2,3,4,5};
    int len2 = sizeof(d)/sizeof(d[0]);
    cout << len2 << endl;  //5
    cout << endl;

    int a[6] = {1,2,3,4,5};
    int len3 = sizeof(a)/sizeof(a[0]);
    cout << len3 << endl;  //6
    cout << endl;

    return 0;
}

使用 sizeof 运算符:可以使用 sizeof(array) / sizeof(array[0]) 来获取数组的长度。这种方法适用于静态数组,在编译时期就已经确定了数组大小。

无论是哪种方式,都只适用于具有已知大小的数组。对于指针指向的动态数组,没有直接的方法获取其长度,需要通过其他手段来记录或传递数组的长度信息。

函数的格式

格式

<返回值类型> 函数名(<参数类型> <参数名>,<参数类型> <参数名>,...){

	函数主体;
	return <变量/值>   //当有<返回值类型>的时候则必须有return进行返回;若无<返回值类型>的时候,则不应该有return语句。无返回值的情况只能出现在main()函数(C++特性)

}

示例:

#include <iostream>

//using namespace std;

void test(int a,int b=2,int c=3);  //若函数的定义时在被调用的地方之后;则必须在调用前组函数的前置申明(否则编译时就会报错,因为编译按顺序执行);若有参数默认值,也应在前置申明中定义。函数的返回值与后文的函数定义中的返回值必须一致。

main(){     //我们的这个主函数即没有返回值类型,也没有参数;没有返回值类型代表函数主体中不应该出现return语句。
    test(10);
}

void test(int a,int b,int c){    //若有参数默认值,也应在前置申明中定义。不应再此处定义。
    std::cout << "test output something(a):" << a << std::endl;    //当前面的using namespace std;语句没被注释时可以省略此处的std::;若无则必须加上std::.

}

void与return

void test(int a,int b,int c){
    std::cout << "test output something(a):" << a << std::endl;
    return;   //注意此处因为返回值类型时void,所以要么此处删除return语句;要么直接些return;。即使return NULL;都不行。return NULL代表返回一个空指针。
}

tips:

C++ 标准规定,如果没有显式指定 main() 函数的返回类型,则默认是 int 类型。这意味着程序可以选择显式声明返回类型,也可以省略该声明,编译器会自动将其视为 int 返回类型。

尽管在大多数情况下,我们会在 main() 函数中使用 return 语句显式地返回一个整数值来指示程序的退出状态,但是即使省略了 return 语句,编译器也会隐式地在 main() 函数末尾插入 return 0; 以表示成功的程序终止。

请注意,在其他函数中,返回类型的声明是必需的,不能省略。只有 main() 函数可以被特殊对待。

建议:

void -> return;或者省略

其他类型 -> return相对应类型的返回值

auto -> 自动适配

函数的参数赋值

#include <iostream>

void printInfo(std::string name, int age, std::string country) {
    std::cout << "Name: " << name << std::endl;
    std::cout << "Age: " << age << std::endl;
    std::cout << "Country: " << country << std::endl;
}

int main() {
    std::string name = "Alice";
    int age = 25;
    std::string country = "China";

    // 使用参数名=赋值的形式调用函数
    printInfo(name, age, country);

    //printInfo(age=30, country="USA", name="Bob");
    //这种写法在C++中错误的;

    return 0;
}

C++ 不支持在调用函数时使用"参数名=赋值"的形式。参数必须按照它们在函数声明中的顺序进行传递。

需要注意的是,C++17 引入了结构化绑定(structured bindings)特性,使得可以通过结构化的方式来解构和绑定多个变量。这可能与某种程度上的命名参数类似。但与提到的"参数名=赋值"的形式还是存在区别。

常量const类型的函数(注意const的位置)

在 C++ 中,可以将类成员函数声明为 const 类型,这表示该函数不会修改对象的状态。使用 const 关键字修饰的成员函数称为常量成员函数。

常量成员函数有以下特点:

  1. 在常量成员函数内部,不能修改类的非静态成员变量,除非它们被声明为 mutable(可变)。
  2. 常量成员函数可以被 const 对象调用,即限定为只读操作。
  3. 常量成员函数不能调用非常量成员函数,因为非常量成员函数可能会修改对象的状态。

以下是一个示例,展示了如何定义和使用常量成员函数:

class MyClass {
public:
    int getValue() const {
        return value;
    }

    void setValue(int newValue) {
        value = newValue;
    }

private:
    int value;
};

int main() {
    const MyClass obj;  // 创建一个常量对象

    int result = obj.getValue();  // 调用常量成员函数

    // obj.setValue(10);  // 错误:常量对象无法调用非常量成员函数

    return 0;
}

在上述示例中,我们定义了一个名为 MyClass 的类,其中包含一个私有成员变量 valuegetValue() 是一个常量成员函数,它返回 value 的值,而 setValue() 是一个非常量成员函数,用于设置 value 的值。

main() 函数中,我们创建了一个常量对象 obj,并使用 obj.getValue() 调用了常量成员函数。注意,我们不能使用 obj.setValue() 修改常量对象的值。

通过使用 const 关键字修饰类成员函数,可以在设计上表明该函数不会修改类对象的状态,以提高代码的可读性和安全性。

void print() const; 和 void const print() 和 const void print();三者的区别

这三个函数声明之间的区别如下:

void print() const;:这是一个成员函数,在函数声明的最后加上了const关键字。它表示该成员函数是一个常量成员函数,意味着在该函数内部不能修改对象的成员变量。这个函数可以在常量对象上调用,也可以在非常量对象上调用。

void const print();:这个函数声明中将const关键字放在了返回类型void之前。这是一种不常见的函数声明方式,但在语法上是合法的。这种声明方式并没有特定的含义,因为const关键字通常用于修饰指针或引用类型,而不是返回类型。

const void print();:这个函数声明中将const关键字放在了函数名print之前。这个声明是非法的,因为在C++中,const关键字只能用于修饰指针、引用、成员函数和成员变量,而不能直接用于返回类型。

综上所述,void print() const;是一个常量成员函数的声明,可以在常量对象和非常量对象上调用。而另外两种声明方式是非法的或者没有特定的含义。(可是在实际代码书写中void const print();const void print();并不会编译错误;这个要在注意。大家可以理解为实际上并不存在既是void又是const的一种事物。)

静态static类型的函数

在 C++ 中,静态函数(也称为静态成员函数)是属于类的函数,而不是属于类的实例或对象。静态函数与类的实例无关,可以直接通过类名调用,而不需要创建类的对象。

静态函数有以下特点:

  1. 它们没有隐式的 this 指针,因为它们不与任何特定的对象联系在一起。
  2. 静态函数只能访问类的静态成员变量和其他静态函数。无法在静态函数中使用非静态成员变量或非静态函数。
  3. 静态函数可以通过类名和作用域解析运算符 :: 直接调用,不需要通过类的实例。
  4. 静态函数可以在类定义内部或外部进行定义和声明。

下面是一个示例,展示了如何定义和使用静态函数:

class MyClass {
public:
    static void staticFunction() {
        // 执行静态函数的操作
    }

    static int staticVariable;  // 静态成员变量声明

private:
    int nonStaticVariable;
};

int MyClass::staticVariable = 0;  // 静态成员变量定义

int main() {
    MyClass::staticFunction();  // 直接通过类名调用静态函数

    MyClass::staticVariable = 10;  // 访问和修改静态成员变量

    return 0;
}

在上述示例中,我们定义了一个名为 MyClass 的类,其中包含一个静态函数 staticFunction() 和一个静态成员变量 staticVariable。在 main() 函数中,我们通过类名调用了静态函数,并访问和修改了静态成员变量。

静态函数常用于以下情况:

  • 执行与类相关的任务,但不依赖于任何特定的实例。
  • 提供一些实用功能,可以直接通过类名调用。
  • 用于访问和修改静态成员变量。

需要注意的是,在静态函数中无法使用非静态成员变量和非静态函数,因为这些成员是与类的实例相关联的。

变量

变量的时(生命周期)空(作用域)性

控制变量生命周期的关键字:

在C++中,有两个关键字可用于控制变量的生命周期和作用域:

  1. autoauto是C++11引入的关键字,用于根据变量的初始值自动推断其类型。它可以帮助简化代码,并使变量的生命周期与初始化值的作用域一致。
  2. staticstatic用于声明静态变量,具有静态生存期,即在程序运行期间保持其值,并且在声明所在的作用域内可见。静态变量通常在全局作用域或函数内部使用。

除此之外,还有其他的关键字用于控制变量的生命周期和作用域,例如:

  1. constconst用于声明一个不可修改的常量。通过将变量声明为const,可以限制对其值的修改,从而控制其生命周期和作用域。
  2. volatilevolatile用于声明一个可能会被意外修改的变量。它告诉编译器不要进行过多的优化操作,以保证每次访问都能获取最新的值。

这些关键字都具有不同的作用和语义,可以根据具体的需求来选择适当的关键字以控制变量的生命周期和作用域。

控制变量作用域的关键字

在C++中,有几个关键字可用于控制变量的作用域:

  1. block scope(块作用域):在代码块内定义的变量具有块作用域,只在当前代码块内有效。这是C++的默认作用域规则。
  2. namespace(命名空间):命名空间是一种组织代码的机制,可以将全局作用域分割为不同的命名空间,并在每个命名空间内定义变量。变量在命名空间内部可见,需要通过命名空间限定符来访问。
  3. local scope(局部作用域):在函数或代码块中定义的变量具有局部作用域,只在所在的函数或代码块内有效。这意味着这些变量只能在其定义的范围内使用。
  4. global scope(全局作用域):在所有函数和代码块外定义的变量具有全局作用域,可以在整个程序的任何地方访问。全局作用域的变量可以被所有函数和代码块访问。

此外,在C++中还可以结合其他关键字来控制变量的作用域,例如:

  • 静态变量(使用static关键字):静态变量具有文件作用域,只在声明它的文件内可见。
  • 外部链接性(使用extern关键字):使用extern关键字声明的变量具有外部链接性,可以在不同的源文件之间共享。

通过合理使用这些关键字,可以控制变量的作用域和可见性,从而实现变量的封装和隔离。

常量const

在C++中,常量被定义为不可修改的值。因此,直接修改常量的值是不允许的,这样做会导致编译错误。

如果您需要在程序运行时修改一个变量的值,并且希望该值在修改后保持不变,可以使用 const 修饰符来声明一个常量,并初始化它的值。例如:

const int constantValue = 10;

在上述代码中,constantValue 被声明为一个常量,并初始化为 10。之后,无法通过任何手段来修改 constantValue 的值。

如果您需要一个可以在运行时更改的值,可以使用变量而不是常量。例如:

int variableValue = 20;

// 修改变量的值
variableValue = 30;

请注意,这里的 variableValue 是一个变量,可以通过赋值操作来修改其值。

总之,在C++中,常量是不可修改的。如果您需要一个可以更改的值,请使用变量而不是常量。

常量地址引用可以修改变量值吗?

常量地址引用不会修改变量的值,因为常量引用本身是指向常量的。一旦将一个变量声明为常量引用,就不能通过该引用来修改变量的值。

以下是一个示例代码,演示了常量引用的应用和限制:

#include <iostream>

int main() {
    int x = 10;
    const int& ref = x; // 常量引用
    int y = ref;

    std::cout << "x: " << x << std::endl;         // 输出:x: 10
    std::cout << "ref: " << ref << std::endl;     // 输出:ref: 10
    std::cout << "y: " << y << std::endl;     // 输出:y: 10

    x = 20; // 修改变量的值

    std::cout << "x (after modification): " << x << std::endl;         // 输出:x (after modification): 20
    std::cout << "ref (after modification): " << ref << std::endl;     // 输出:ref (after modification): 20
    std::cout << "y: " << y << std::endl;     // 输出:y: 10 <--

    // ref = 30; // 错误!无法通过常量引用修改变量的值 <--

    y = 40; // 修改变量的值

    std::cout << "x (after modification): " << x << std::endl;         // 输出:x (after modification): 20  <--
    std::cout << "ref (after modification): " << ref << std::endl;     // 输出:ref (after modification): 20  <--
    std::cout << "y: " << y << std::endl;     // 输出:y: 40 <--

    return 0;
}

在上述代码中,const int& ref 是一个常量引用,它引用了变量 x。初始时,xref 都是 10。然后通过修改变量 x 的值,xref 变成了 20。但是,尝试通过常量引用 ref 来修改变量的值是错误的,会导致编译错误。

总结起来,常量地址引用不允许修改变量的值,它只提供对常量的只读访问。要修改变量的值,应使用非常量引用或直接操作变量本身。

形参与实参

在编程语言中,形参(形式参数)和实参(实际参数)是函数定义和函数调用过程中使用的两个相关概念。

形参(形式参数)是函数定义中用于接收传入值的占位符或变量名。它在函数定义时被声明,并且用于指定函数所需的输入参数的类型和名称。形参相当于函数内部的局部变量,在函数体内可以被使用。形参的值由实参提供。

实参(实际参数)是在函数调用时传递给函数的具体值或表达式。它是函数调用时传递给形参的真实数据。实参可以是常量、变量、表达式或者其他函数的返回值。

下面是一个简单的示例代码,演示了形参和实参的使用:

#include <iostream>

// 函数定义,带有两个形参(a,b)
void addNumbers(int a, int b) {
    int sum = a + b;
    std::cout << "Sum = " << sum << std::endl;
}

int main() {
    int x = 5;
    int y = 3;

    // 函数调用,使用实参(x,y)传递给形参(a,b)
    addNumbers(x, y);

    return 0;
}

在上述代码中,addNumbers() 函数定义了两个形参 ab,用于接收传入的值。然后,在 main() 函数中调用 addNumbers() 函数,并传递变量 xy 作为实参。在函数调用过程中,实参 x 的值被传递给形参 a,实参 y 的值被传递给形参 b。函数体内部使用这些形参计算两个数的和,并输出结果。

总结来说,形参是函数定义时声明的参数占位符,用于接收函数调用时传递的实际参数的值。实参是函数调用时传递给形参的具体值或表达式。

函数的引用

在C++中,函数引用是一种特殊类型的引用,它允许我们在函数中使用其他变量或对象的别名。通过函数引用,我们可以在函数内部直接操作原始变量,而无需进行值的复制。

要声明函数引用,需要在函数参数列表中使用引用符号(&)来指示引用。函数引用在传递实参时会绑定到相应的变量或对象上,并成为该变量或对象的一个别名。对函数引用的操作会直接影响原始变量或对象。

以下是一个简单的示例代码,演示了函数引用的使用:

#include <iostream>

// 函数通过引用交换两个整数的值
void swap(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}

int main() {
    int x = 5;
    int y = 10;

    std::cout << "Before swap: x = " << x << ", y = " << y << std::endl;

    // 通过函数引用交换变量的值
    swap(x, y);

    std::cout << "After swap: x = " << x << ", y = " << y << std::endl;

    return 0;
}

在上述代码中,swap() 函数使用了两个参数 ab 的引用。函数体内部使用临时变量 temp 来交换 ab 的值。在 main() 函数中,我们定义了变量 xy,并输出它们的初始值。然后通过调用 swap() 函数来交换这两个变量的值。由于函数参数是通过引用传递的,所以在函数内部对参数的操作会直接修改原始变量的值。最后,我们再次输出交换后的变量值。

总结起来,函数引用在C++中允许我们通过别名对函数内部的变量或对象进行操作,而无需进行值的复制。它可以方便地修改传入的参数,并且提高了程序的效率。

指针与引用

指针和引用在C++中有以下区别:

  1. 定义和初始化:指针需要使用*符号来声明和解引用,而引用则通过在声明时使用&符号进行定义。
int x = 42;
int* ptr = &x;  // 指针的声明和初始化
int& ref = x;   // 引用的声明和初始化
  1. 可空性:指针可以为空(null),即指向一个无效的地址。而引用必须在声明时绑定到有效的对象上,并且不能为null。
  2. 重新赋值:指针可以被重新赋值以指向不同的对象。换句话说,指针可以改变其所指向的对象。而引用一旦绑定了一个对象,就不能再改变其绑定的对象,它始终引用同一个对象。
int x = 42;
int y = 10;
int* ptr = &x;
int& ref = x;

ptr = &y;  // 更改指针的指向,现在ptr指向y
ref = y;   // 修改ref所引用的变量x的值为y
  1. 空间占用:指针本身需要占用内存空间来存储地址信息。而引用只是变量的别名,并没有自己的内存空间。
  2. 对象关系:指针可以指向任意类型的对象(包括基本类型、数组、结构体、对象等),也可以指向空类型(void*)。而引用只能绑定到与其类型相同的对象。
  3. 空间传递:指针可以在函数之间传递地址,以进行参数传递和返回。而引用通常用于函数参数传递或作为函数返回值,以提供对变量的直接访问方式,避免了指针传递中需要解引用的繁琐操作。
void modifyByPointer(int* ptr) {
    // 通过指针修改传入的变量
    *ptr = 10;
}

void modifyByReference(int& ref) {
    // 通过引用修改传入的变量
    ref = 10;
}

int x = 42;
modifyByPointer(&x);    // 通过指针修改x的值
modifyByReference(x);   // 通过引用修改x的值

总的来说,指针提供了更大的灵活性和间接性,可以动态分配内存并改变所指向的对象。引用更直接且简洁,提供了对原始变量的访问。

指针占用内存吗?

是的,指针在内存中占用一定的空间。在大多数计算机体系结构中,指针的大小是固定的,通常与操作系统的位数相关。

在32位系统上,一个指针的大小通常为4字节(32位),而在64位系统上,一个指针的大小通常为8字节(64位)。这意味着每个指针变量会使用相应的字节数来存储内存地址。

无论指针实际指向何处,它都需要有足够的空间来存储内存地址。因此,当您声明指针变量时,系统将分配一定的内存来存储指针,并根据需要进行适当的内存管理。

引用占用内存吗?

引用在内存中不占用额外的空间。引用是对变量的别名,它使用的是被引用变量的内存地址作为标识符,因此引用并不需要分配独立的存储空间。

当您声明一个引用时,实际上是在给变量创建一个别名,它与原始变量共享同一块内存。这意味着对引用的操作实际上就是对原始变量的操作,没有额外的内存开销。

由于引用没有独立的存储空间,所以不能将引用指向另一个对象或者重新绑定到其他变量。一旦引用被初始化绑定到某个对象,就无法改变其指向。

因此,可以说引用实际上是对已存在变量的别名,并不会占用额外的内存空间。

示例:

#include <iostream>
#include <typeinfo>

using namespace std;

int main(){
    int a = 5;
    int b = 10;
    char x = 'x';
    cout<<"a's vaule:"<< a << endl;
    cout<<"a's address:"<< &a << endl;
    cout<<"a's type name:"<< typeid(a).name()<<endl;
    cout<<"b's vaule:"<< b << endl;
    cout<<"b's address:"<< &b << endl;
    cout<<"b's type name:"<< typeid(b).name()<<endl;
    cout<<"x's vaule:"<< x << endl;
    cout<<"x's address:"<< &x << endl;
    cout<<"x's type name:"<< typeid(x).name()<<endl;
    cout<<"----------"<< endl;

    // int* c = a;   //错误,指针只能被赋予地址
    int* c = &a;
    cout<<"c's vaule:"<< c << endl;
    cout<<"c's address:"<< &c << endl;
    cout<<"c's type name:"<< typeid(c).name()<<endl;
    cout<<"pointer c's pointing vaule:"<< *c << endl;
    cout<<"----------"<< endl;

    c = &b;   //可以将指针c重新赋值哦;不同于ref。
    cout<<"c's vaule:"<< c << endl;
    cout<<"c's address:"<< &c << endl;
    cout<<"c's type name:"<< typeid(c).name()<<endl;
    cout<<"pointer c's vaule:"<< *c << endl;
    cout<<"----------"<< endl;

    // c = &x;    //错误写法,以为变量c被声明为整型指针,但x是char类型。error: cannot convert 'char*' to 'int*' in assignment

    int* d;
    cout<<"d's vaule:"<< d << endl;
    cout<<"d's address:"<< &d << endl;
    cout<<"d's type name:"<< typeid(d).name()<<endl;
    // cout<<"pointer d's vaule:"<< *d << endl;     //对于空指针,在这里会出错。
    cout<<"----------"<< endl;

    d = &a;
    cout<<"d's vaule:"<< d << endl;
    cout<<"d's address:"<< &d << endl;
    cout<<"d's type name:"<< typeid(d).name()<<endl;
    cout<<"pointer d's vaule:"<< *d << endl;
    cout<<"----------"<< endl;

    const int* e = &b;
    cout<<"e's vaule:"<< e << endl;
    cout<<"e's address:"<< &e << endl;
    cout<<"e's type name:"<< typeid(e).name()<<endl;
    cout<<"pointer e's vaule:"<< *e << endl;
    cout<<"----------"<< endl;

    e= &a;
    cout<<"e's vaule:"<< e << endl;
    cout<<"e's address:"<< &e << endl;
    cout<<"e's type name:"<< typeid(e).name()<<endl;
    cout<<"pointer e's vaule:"<< *e << endl;
    cout<<"----------"<< endl;

    // *e = 100;    //这种写法是错误的;因为const int* e = &b;限定了指针变量e是一个指向常量整型数据的指针;既然是指向了一个常量(本例中指向的变量a实际上不是常量,但申明指针式加上了常量的限定),那么就不能通过任何任何性质包括*e来修改这个常量的值。



    int* const f = &b;
    cout<<"f's vaule:"<< f << endl;
    cout<<"f's address:"<< &f << endl;
    cout<<"f's type name:"<< typeid(f).name()<<endl;
    cout<<"pointer f's vaule:"<< *f << endl;
    cout<<"----------"<< endl;
    
    /*
    错误写法;因为int* const f = &b;代表变量f是常量,类型是指针;因为是常量所以不能被重新赋值。
    f = &a;
    cout<<"f's vaule:"<< f << endl;
    cout<<"f's address:"<< &f << endl;
    cout<<"f's type name:"<< typeid(f).name()<<endl;
    cout<<"pointer f's vaule:"<< *f << endl;
    cout<<"----------"<< endl;
    */

    *f = 100;
    cout<<"f's vaule:"<< f << endl;
    cout<<"f's address:"<< &f << endl;
    cout<<"f's type name:"<< typeid(f).name()<<endl;
    cout<<"pointer f's vaule:"<< *f << endl;
    cout<<"----------"<< endl;
    cout<<"b's vaule:"<< b << endl;
    cout<<"b's address:"<< &b << endl;
    cout<<"b's type name:"<< typeid(b).name()<<endl;
    cout<<"----------"<< endl;


    const int* const g = &b;
    cout<<"g's vaule:"<< g << endl;
    cout<<"g's address:"<< &g << endl;
    cout<<"g's type name:"<< typeid(g).name()<<endl;
    cout<<"pointer g's vaule:"<< *g << endl;
    cout<<"----------"<< endl;
    
    /*
    错误写法;因为const int* const g = &b;;代表变量g是常量(后一个const),类型是指针;因为是常量所以不能被重新赋值。
    g = &a;
    cout<<"g's vaule:"<< g << endl;
    cout<<"g's address:"<< &g << endl;
    cout<<"g's type name:"<< typeid(g).name()<<endl;
    cout<<"pointer g's vaule:"<< *g << endl;
    cout<<"----------"<< endl;
    */

   // *g = 200;       //这种写法是错误的;因为const int* const g = &b;的前一个const限定了指针变量g是一个指向常量整型数据的指针;既然是指向了一个常量(本例中指向的变量a实际上不是常量,但申明指针式加上了常量的限定),那么就不能通过任何任何性质包括*e来修改这个常量的值。


}

引用调用

示例

#include <iostream>

void increment(int& num) {
    std::cout << "The value of num: " << num << std::endl;
    std::cout << "The address of num: " << &num << std::endl;
    num++;  // 对传入的num进行递增操作
}

int main() {
    int a = 10;
    std::cout << "The address of a: " << &a << std::endl;
    std::cout << "Before increment: " << a << std::endl;
    increment(a);  // 传递a的引用给increment函数
    std::cout << "After increment: " << a << std::endl;

    return 0;
}

输出结果:

The address of a: 0x61fe1c
Before increment: 10
The value of num: 10
The address of num: 0x61fe1c
After increment: 11

引用与指针的区别

引用和指针是 C++ 中的两种不同的概念,它们在用法和语义上有一些区别。

  1. 定义:引用是一个别名,用于引用已存在的变量。指针是一个变量,它存储了另一个变量的地址。
  2. 初始化和赋值:引用必须在定义时进行初始化,并且一旦初始化后,它将一直引用同一个对象,无法重新指向其他对象。指针可以在定义时不进行初始化,也可以随时改变所指向的对象。
  3. 空值:引用不能为空,必须引用某个已存在的对象。指针可以为空,即指向空指针(nullptr)。
  4. 运算符:可以对指针使用运算符(例如解引用、取址、递增、递减等),以操作所指向的对象。而对于引用,它本身不是一个独立的对象,因此没有这些针对引用的特定运算符。
  5. 空间占用:指针需要占用内存来存储所指向对象的地址,而引用不需要额外的内存空间。
  6. 安全性和易用性:由于引用必须在定义时进行初始化并且不能重新指向其他对象,它更安全且易于使用。指针的使用需要更谨慎,因为它可以包含空值或者指向无效的地址。

总结来说,引用提供了一种更直接、更简单和更安全的方式来操作变量,而指针更为灵活,但也需要更加小心地处理。在选择使用引用还是指针时,需要根据具体的情况和需求来决定。

示例:

#include <iostream>
using namespace std;
int main() {
    int x = 42;
    int y = 10;
    int* ptr = &x;
    int& ref = x;
    cout << "ptr中存放的地址(这里是指x变量的内存地址):" << ptr << endl;
    cout << "ptr存放的地址所对应的值:" << *ptr << endl;
    cout << "ref对应的地址:" << &ref << endl;
    cout << "ref代表的值:" << ref << endl;

    ptr = &y;  // 更改指针的指向,现在ptr指向y
    ref = y;   // 修改ref所引用的变量x的值为y

    return 0;
}

输出结果:

ptr中存放的地址(这里是指x变量的内存地址):0x61fe0c
ptr存放的地址所对应的值:42
ref对应的地址:0x61fe0c
ref代表的值:42

强制类型转换

在C++中,可以使用类型转换(Type Casting)将一个数据类型转换成另一个数据类型。下面是几种常见的类型转换方法:

  1. 隐式类型转换(Implicit Type Conversion):这是自动进行的类型转换,在不丢失精度或超出范围的情况下,根据需要自动将一种类型转换为另一种类型。例如,将整数赋值给浮点数变量时,会发生隐式的类型转换。
  2. 强制类型转换(Explicit Type Conversion):这是手动执行的类型转换,可以明确指定将一个类型转换为另一个类型。C++提供了三种强制类型转换运算符:
  • 静态转换(Static Cast):用于不具备继承关系的类型之间的转换,例如将浮点数转换为整数、指针类型之间的转换等。
  • 动态转换(Dynamic Cast):用于具有继承关系的类型之间的转换,进行运行时类型检查和安全检查,可用于父子类之间的指针或引用的转换。
  • 常量转换(Const Cast):用于去除变量的常量性质,主要用于处理const限定符。
  • 重新解释转换(Reinterpret Cast):用于将一个类型的位模式重新解释为另一个类型的位模式,这是一种低级的、不安全的类型转换。

注意:在进行类型转换时要小心,确保转换的结果符合语义和逻辑。错误的类型转换可能导致程序运行异常或产生意外的结果。

请看下面的示例:

  1. 隐式类型转换(Implicit Type Conversion):
int num = 10;
double result = num;   // 将整数类型隐式转换为浮点数类型
  1. 强制类型转换(Explicit Type Conversion):
double d = 3.14;
int i = static_cast<int>(d);     // 使用静态转换将浮点数转换为整数

class Base {
    virtual void foo() {}
};
class Derived : public Base {};

Base* basePtr = new Derived();
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);   // 使用动态转换将基类指针转换为派生类指针

const int* constPtr = new int(42);
int* nonConstPtr = const_cast<int*>(constPtr);   // 使用常量转换去除常量性质

int value = 65;
char ch = reinterpret_cast<char>(value);   // 使用重新解释转换,将整数类型的值重新解释为字符类型

上述示例展示了隐式类型转换和强制类型转换的常见用法。请谨慎使用强制类型转换,确保在类型转换过程中不会出现问题。

综合示例

#include <iostream>
#include <string>

using namespace std;

int main(){
    int a = 10;
    double b = 3.14;
    //string c = "hello";    // "abc"、"hello"等string不能被转化为int;需要使用"123"/"oxabc"/"0b10110101"
    string c = "232";
    const int d = 20;

    //强制类型转化
    cout << "---转换前---" << endl;
    cout << "变量a的类型是:" << typeid(a).name() << ";值为:" << a << endl;
    cout << "变量b的类型是:" << typeid(b).name() << ";值为:" << b << endl;
    cout << "变量c的类型是:" << typeid(c).name() << ";值为:" << c << endl;
    cout << "变量d的类型是:" << typeid(d).name() << ";值为:" << d << endl;

    cout << endl;
    cout << "---开始转换---" << endl;
    double aa = a;    //隐式转化 implicit type conversion;  int -> double
    int bb = static_cast<int>(b);       //显式转换  explicit type conversion; double -> int
    int cc = std::stoi(c);              //显式转换; string -> int;注意这里如果c是不能转换为int的字符串是程序会闪退。
    string dd = std::to_string(d);      //显式转换; int -> string



    cout << endl;
    cout << "---转换后---" << endl;
    cout << "变量aa的类型是:" << typeid(aa).name() << ";值为:" << aa << endl;
    cout << "变量bb的类型是:" << typeid(bb).name() << ";值为:" << bb << endl;
    cout << "变量cc的类型是:" << typeid(cc).name() << ";值为:" << cc << endl;
    cout << "变量dd的类型是:" << typeid(dd).name() << ";值为:" << dd << endl;
    return 0;
}

输出结果:

---转换前---
变量a的类型是:i;值为:10
变量b的类型是:d;值为:3.14
变量c的类型是:NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE;值为:232
变量d的类型是:i;值为:20

---开始转换---

---转换后---
变量aa的类型是:d;值为:10
变量bb的类型是:i;值为:3
变量cc的类型是:i;值为:232
变量dd的类型是:NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE;值为:20

const int* e和int* const e的区别

const int* eint* const e 是 C/C++ 中的两种不同的指针声明方式,它们有着不同的含义和用法。

  1. const int* e: 这个声明表示 e 是一个指向常量整型数据的指针。也就是说,通过 e 可以读取被指向的整型数据,但不能通过 e 修改被指向的数据。这种声明方式通常用于指向常量数据的指针,强调指针所指向的内容应该是只读的。
    示例代码:
const int* e;  // e是指向常量整型数据的指针

int num = 10;
e = #      // 允许将指向非常量的地址赋给指针
int value = *e;  // 允许读取被指向的整型数据

// 不允许通过指针修改被指向的数据
*e = 20;        // 错误,尝试修改常量数据会导致编译错误
  1. int* const e: 这个声明表示 e 是一个指向整型数据的常量指针。也就是说,e 在声明后就不能再指向其他内存地址,它的值在初始化后就固定了。但可以通过 e 修改被指向的整型数据。这种声明方式通常用于指向可修改的数据,同时要求指针本身的值固定不变
    示例代码:
int* const e;  // e是一个常量指针,它的值在初始化后就不能再改变

int num1 = 10;
int num2 = 20;
e = &num1;     // 初始化指针,使其指向num1
*e = 30;       // 修改被指向的整型数据

// 不允许修改指针本身的值
e = &num2;     // 错误,尝试修改常量指针的值会导致编译错误

综上所述,const int* e 声明的是一个指向常量整型数据的指针,而 int* const e 声明的是一个指向整型数据的常量指针。两者的不同之处在于,前者强调无法通过指针修改被指向的数据,后者强调指针自身的值固定不变。

流程控制语句

三元运算符 ?:

C++ 中的条件运算符 ?:,也称为三元运算符(ternary operator),可以根据给定的条件选择执行不同的表达式。它的语法如下:

condition ? expression1 : expression2

如果 condition 为真,则整个表达式的值为 expression1 的值;如果 condition 为假,则整个表达式的值为 expression2 的值。其中,condition 是一个条件表达式,可以是任何能够求值为布尔值(truefalse)的表达式。

以下是一个使用条件运算符的示例:

#include <iostream>
using namespace std;

int main() {
    int num = 10;
    string message;

    // 使用条件运算符根据条件赋值
    message = (num > 0) ? "Positive number" : "Negative number or zero";

    cout << message << endl;  // 输出: Positive number

    return 0;
}

在上述示例中,我们定义了一个整型变量 num 和一个字符串变量 message。通过条件运算符将相应的消息赋值给 message 变量。

num 大于 0 时,条件 (num > 0) 为真,因此整个条件运算符表达式的值为 "Positive number",将其赋给 message。最后,输出 message 的值为 "Positive number"

条件运算符可用于在单个语句中实现简单的条件逻辑,避免编写 if-else 结构的冗长代码。

条件语句:

if语句:

if (条件表达式) {
    // 如果条件为真,执行的代码
} else {
    // 如果条件为假,执行的代码
}

示例:

int num = 10;
if (num > 0) {
    cout << "num是正数" << endl;
} else {
    cout << "num是负数或零" << endl;
}

switch语句:

在C++中,switch case语句用于根据不同的情况执行不同的代码块。它可以替代多个if-else if语句,使代码更简洁和易读。

switch case语句的语法如下:

switch (表达式) {
    case 值1:
        // 执行的代码
        break;
    case 值2:
        // 执行的代码
        break;
    default:
        // 执行的代码(可选)
}

示例:

int dayOfWeek = 1;
switch (dayOfWeek) {
    case 1:
        cout << "星期一" << endl;
        break;
    case 2:
        cout << "星期二" << endl;
        break;
    case 3:
        cout << "星期三" << endl;
        break;
    default:
        cout << "其他星期几" << endl;
}

如果在switch case语句中没有使用break语句来结束某个case的代码块,那么程序会继续执行下一个case的代码块,而不会跳出switch语句。 下面是一个没有使用break语句的示例:

#include <iostream>
using namespace std;

int main() {
    int x = 2;

    switch (x) {
        case 1:
            cout << "x 等于 1" << endl;
        case 2:
            cout << "x 等于 2" << endl;
        case 3:
            cout << "x 等于 3" << endl;
        default:
            cout << "默认情况" << endl;
    }
    
    return 0;

}

在这个示例中,变量x的值为2。由于在case 1的代码块中没有使用break语句来结束,因此程序会继续执行下一个case的代码块。所以输出结果会是:

x 等于 2
x 等于 3
默认情况

注意,如果在某个case的代码块中没有使用break语句,那么后续所有的case的代码块都会被执行,直到遇到下一个break语句或者到达switch语句的结束。这种情况下,我们称之为"case穿透"或"fall-through"。如果不希望发生"case穿透",需要在每个case的代码块中使用break语句来显式地结束当前的case。

循环语句:

for循环:

for (初始化表达式; 条件表达式; 更新表达式) {
    // 循环体代码
}

示例:

for (int i = 0; i < 5; i++) {
    cout << i << endl;
}

while循环:

while (条件表达式) {
    // 循环体代码
}

示例:

int i = 0;
while (i < 5) {
    cout << i << endl;
    i++;
}

do-while循环:

do {
    // 循环体代码
} while (条件表达式);

示例:

int i = 0;
do {
    cout << i << endl;
    i++;
} while (i < 5);

重载

一般函数重载

示例:

#include <iostream>

using namespace std;

void test(int a, int b){
    cout<<"i'm test001 function"<<endl;

}

/*
仅仅参数名的不同不能构成与test001构成重载;编译时会报错。
void test(int c, int d){
    cout<<"i'm test002 function"<<endl;

}
*/


/*
多出来的参数因为有指定默认值,导致砸调用时程序无法确定使用的是哪个test函数;因此这种写法也不能正确构成与test001的重载。
在函数调用时会产生二义性。
void test(int a, int b, int c=100){
    cout<<"i'm test003 function"<<endl;

}
*/


void test(int a, int b, int c){
    cout<<"i'm test004 function"<<endl;

}

void test(int a, char b, int c){
    cout<<"i'm test005 function"<<endl;

}


int main(){
    int x = 10;
    int y = 20;
    test(x, y);
    test(x, y, 50);
    test(x, 'b', 50);

    return 0;
}

运算符重载

待更...

单独章节详解。

C++中重载和覆盖(多态的实现基础之一)的区别

C++中函数的覆盖(override)和重载(overload)是不同的概念。

函数的覆盖指的是派生类在继承基类时,重新定义(override)了基类中的虚函数。通过使用virtual关键字声明基类函数,并在派生类中使用相同的函数名和参数列表进行重新定义,可以实现函数的覆盖。当通过基类指针或引用调用该函数时,会根据实际对象的类型来执行对应的重定义函数。

函数的重载指的是在同一个作用域内,根据不同的参数类型、参数个数或参数顺序,定义具有相同名称但不同参数的多个函数。重载函数之间彼此独立,它们可以具有不同的功能和实现。

函数的覆盖和重载的区别可以总结如下:

  1. 覆盖涉及到派生类和基类之间的继承关系,重载仅在同一作用域内。
  2. 覆盖要求函数必须是虚函数,重载没有此要求。
  3. 覆盖发生在运行时,根据实际对象类型进行动态绑定,重载发生在编译时,根据调用参数进行静态选择。
  4. 覆盖涉及到单一函数的不同实现,重载涉及到多个函数的同名多态。

需要注意的是,函数的覆盖和重载是不互斥的,可以同时在程序中使用。

overload的例子:

#include <iostream>
using namespace std;

void print(int num) {
    cout << "Printing an integer: " << num << endl;
}

void print(double num) {
    cout << "Printing a double: " << num << endl;
}

int main() {
    int a = 5;
    double b = 3.14;

    print(a); // 调用print(int)
    print(b); // 调用print(double)

    return 0;
}

输出结果:

Printing an integer: 5
Printing a double: 3.14

override的例子:

#include <iostream>
using namespace std;

class Shape {
public:
    virtual void draw() {
        cout << "Drawing a shape." << endl;
    }
};

class Rectangle : public Shape {
public:
    void draw() override {
        cout << "Drawing a rectangle." << endl;
    }
};

class Circle : public Shape {
public:
    void draw() override {
        cout << "Drawing a circle." << endl;
    }
};

int main() {
    Shape* shapePtr = nullptr;

    Rectangle rectangle;
    Circle circle;

    // 使用基类指针调用派生类对象的覆盖函数;特别注意,这是实现多态的基础之一。
    shapePtr = &rectangle;
    shapePtr->draw();  // 输出: Drawing a rectangle.

    shapePtr = &circle;
    shapePtr->draw();  // 输出: Drawing a circle.

    return 0;
}

输出结果:

Drawing a rectangle.
Drawing a circle.

在上述示例中,Shape 是基类,RectangleCircleShape 的派生类。基类 Shape 中声明了一个虚函数 draw(),并在派生类中重新定义(override)了该函数。

main 函数中,我们创建了一个 Shape 类型的指针 shapePtr,并且通过将它指向 Rectangle 对象和 Circle 对象来实现多态性。

当我们通过基类指针 shapePtr 调用 draw() 函数时,根据实际对象类型的不同,会动态绑定到相应的派生类函数。由于函数是通过虚函数实现的,所以可以实现正确的函数覆盖,输出相应形状的绘制信息。

在示例中,当 shapePtr 指向 Rectangle 对象时,调用 draw() 输出 "Drawing a rectangle.";当 shapePtr 指向 Circle 对象时,调用 draw() 输出 "Drawing a circle."。这就体现了函数覆盖和多态性的特点。

C++中隐藏和覆盖(多态的实现基础之一)的区别

在 C++ 中,如果不使用虚函数,是无法实现覆盖的。派生类中定义的同名函数会隐藏基类中的同名函数,而不是覆盖它。这种情况下,调用基类指针或引用时只能访问到基类中对应的函数,而无法调用派生类中的函数。

以下是一个示例来说明隐藏这个概念:

#include <iostream>
using namespace std;

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

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

int main() {
    Base base;
    Derived derived;

    Base* ptr = &base;
    ptr->func(); // 输出: Base::func()

    ptr = &derived;
    ptr->func(); // 输出: Base::func()

    derived.func(); //输出:Derived::func()
    return 0;
}

在这个示例中,Base类有一个名为func()的函数,Derived类继承自Base并在派生类中重新定义了同名函数。然而,由于func()函数没有声明为虚函数,所以在通过基类指针调用该函数时,并不会动态绑定到派生类的函数上,而是仅仅调用了基类的函数。

因此,即使我们将基类指针指向派生类对象,调用func()函数时,输出结果仍然是基类的函数。这就是隐藏(hiding)而不是覆盖(override)。要实现函数的覆盖并实现多态性,需要使用虚函数。

面向对象OOB

对象:

  • 状态(属性
  • 行为(操作/方法/服务/函数)

通过对事务的抽象找出同一类对象的共同属性(静态特征)和行为(动态特征);从而得到类的概念。对象是类的一个具象,类是对象的一个抽象。

基本特点:

  • 抽象
  • 封装
    通过类来实现
  • 继承
    现有类的基础上申明新的类;原有的类叫基类/父类或超类;新类叫派生类或子类
  • 多态
    不同种类的对象都具有名称相同的行为;但具体的行为实现方式却不同。(本质是通过函数的重载或运算符重载来实现的。)

类的函数

  1. 在类体内定义的函数;默认是内联函数。
  2. 也可以在类体内部申明函数并加上inline关键字,然后在类体的外部完成函数的定义。这样的函数也叫内联函数。

下面是一个示例,展示了如何在类体外定义类的函数:

// 类的声明
class MyClass {
private:
    int myData;

public:
    // 函数的声明
    void setData(int data);   //函数主体虽然在类的外部定义;但是在类体中必须要有改函数的原型申明。
    int getData();
};

// 在类体外定义函数
// 注意格式:<返回类型> <类名>::<函数名>(参数类型 参数名){ 函数定义;}
void MyClass::setData(int data) {   
    myData = data;
}

int MyClass::getData() {
    return myData;
}

// 主函数
int main() {
    MyClass obj;
    obj.setData(42);
    int value = obj.getData();
    // 输出结果
    cout << "Value: " << value << endl;
    
    return 0;
}

在上面的示例中,MyClass 是一个简单的类,具有私有成员变量 myData 和公共成员函数 setDatagetData

注意通过在类体外部定义函数,我们使用 ClassName::(例如 MyClass::)来指定函数所属的类。这样编译器就知道函数是属于哪个类的成员函数。

在主函数中,我们创建了一个 MyClass 对象 obj,并使用 setData 方法设置成员变量的值为 42。然后,我们使用 getData 方法获取成员变量的值,并将其输出。

这种方式可以提高代码的可读性和可维护性,将函数的实现与类的声明分离开来,使得类定义更加清晰和模块化。

注意:C++中可以定义不属于任何类的成员函数,这样的函数也叫"全局函数"。

运算符

::

::类作用域运算符

在C++中,::是作用域解析运算符(Scope Resolution Operator),具有以下两个主要功能:

  1. 访问命名空间成员:通过::可以访问命名空间中的成员,包括变量、函数、类等。它允许在命名空间内部定义相同名称的成员,并通过指定命名空间来区分它们。
    例如,假设有一个命名空间example中定义了一个函数doSomething()和一个变量x,可以使用example::doSomething()example::x来访问这些成员。
  2. 访问类的静态成员:对于类的静态成员(静态变量和静态函数),可以使用::来访问它们,不需要通过类的实例来调用。
    例如,如果有一个类MyClass定义了一个静态变量count和一个静态函数staticFunction(),可以使用MyClass::countMyClass::staticFunction()来访问它们,而无需创建类的对象实例。

除此之外,::还可以用于特殊的情况,如全局作用域下的变量访问、基类和派生类之间成员的访问等。在这些情况下,::提供了一种明确指定作用域的方式,以避免命名冲突或引用错误的成员。

>>

>>输入流提取运算符

<<

<<输出流插入运算符

内联函数与外联函数

内联函数(inline function)和外联函数(external linkage function)是C++中两种函数的不同定义和链接方式。

  1. 内联函数(inline function):
  • 内联函数在函数声明处使用关键字 inline 进行声明,通常将函数定义与函数声明放在一起。
  • 内联函数的主要目的是提高程序的执行效率。当调用内联函数时,编译器会将函数的代码插入到调用处,而不是通过函数调用的方式执行。
  • 内联函数适合用于较短、频繁调用的函数,减少了函数调用的开销,但也增加了代码的体积。
  • 注意,内联函数只是对编译器建议的提示,编译器会自行决定是否进行内联优化。

示例:

// 内联函数的定义与声明
inline int add(int a, int b) {
    return a + b;
}

int main() {
    // 调用内联函数
    int result = add(3, 4);
    return 0;
}
  1. 外联函数(external linkage function):
  • 外联函数是普通的函数定义方式,在函数声明和函数定义分开的情况下使用。
  • 外联函数具有外部链接属性,默认情况下可以通过其他源文件中的函数声明进行调用。
  • 外联函数的定义和声明通常放在头文件中,以便在多个源文件中共享和使用。

示例:

// 外联函数的声明
int multiply(int a, int b);

// 外联函数的定义
int multiply(int a, int b) {
    return a * b;
}

int main() {
    // 调用外联函数
    int result = multiply(3, 4);
    return 0;
}

总结:

  • 内联函数通过将函数的代码插入到调用处提高执行效率,适合用于短小且频繁调用的函数。
  • 外联函数是普通的函数定义方式,并具有默认的外部链接属性,可以在其他源文件中进行调用。

对象成员的访问

在C++中,通过点号(.)和指针(->)可以访问对象的成员,但它们有不同的用法和行为。

  1. 点号访问成员:
  • 当使用对象或引用作为访问的目标时,可以使用点号来直接访问成员。
  • 使用点号时,左侧必须是一个具体的对象或引用,而不能是指针。
  • 点号用于从对象中获取成员的值,调用成员函数或访问成员变量。
// 使用点号访问成员的示例
MyClass obj;
obj.memberVar = 42;      // 设置对象的成员变量
int value = obj.memberVar;     // 获取对象的成员变量的值

obj.memberFunc();        // 调用对象的成员函数
  1. 指针访问成员:
  • 当使用指针指向对象时,需要使用箭头操作符(->)来访问成员。
  • 使用箭头操作符时,左侧必须是一个指针,它指向一个对象。
  • 箭头操作符用于从指针指向的对象中获取成员的值,调用成员函数或访问成员变量。
// 使用指针访问成员的示例
MyClass* ptr = new MyClass();   // 创建指向对象的指针

ptr->memberVar = 42;     // 设置指针指向的对象的成员变量
int value = ptr->memberVar;    // 获取指针指向的对象的成员变量的值

ptr->memberFunc();      // 调用指针指向的对象的成员函数

delete ptr;     // 释放之前动态分配的对象内存

总结:

  • 点号(.)用于从对象或引用中直接访问成员。
  • 指针箭头(->)用于通过指针间接访问对象的成员。

需要注意的是,在使用指针访问成员时,确保指针不为空指针,否则会导致未定义的行为。同时,当使用指针动态分配对象时,应该负责手动释放对象内存以防止内存泄漏。

对象成员的访问范围限制

抱歉,我在前面的回答中没有提供完整的示例代码。让我来详细说明类成员的访问范围。

在C++中,类成员可以具有不同的访问级别,包括公有(public)、私有(private)和受保护(protected)。这些访问级别控制了对类成员的访问权限。

  1. 公有访问(public):
  • 公有成员可以被类的外部代码访问。
  • 在类的内部和外部都可以使用点号或箭头操作符直接访问公有成员。
  • 公有成员可以在类的派生类中访问。
  1. 私有访问(private):
  • 私有成员只能在类的内部访问,类的外部代码无法直接访问私有成员。
  • 私有成员可以通过公有成员函数间接访问。通常情况下,私有成员变量会通过公有成员函数进行读取和写入操作,以实现对私有成员的间接访问。
  1. 受保护访问(protected):
  • 受保护成员在类的内部可以访问,在类的外部不能直接访问。
  • 受保护成员可以在派生类内部访问。
  • 类似于私有成员,受保护成员通常通过公有成员函数进行访问。

下面是一个完整的示例:

class MyClass {
public:
    int publicVar;     // 公有成员变量
    void publicFunc()  // 公有成员函数
    {
        // 访问公有、私有和受保护成员
        publicVar = 1;
        privateVar = 2;
        protectedVar = 3;
    }

private:
    int privateVar;    // 私有成员变量

protected:
    int protectedVar;  // 受保护成员变量
};

int main() {
    MyClass obj;
    obj.publicVar = 1;     // 可以直接访问公有成员
    obj.publicFunc();      // 可以调用公有成员函数

    // 下面的代码将无法编译,因为私有成员无法在类外部直接访问
    // obj.privateVar = 2;
    // obj.protectedVar = 3;

    return 0;
}

在上述示例中,公有成员可以直接在类的外部访问,而私有成员和受保护成员只能在类的内部通过成员函数访问。请注意,在派生类中,受保护成员可以被访问。

友元函数对private成员的访问

基本概念

友元实际上并不是面向对象的特征,而是为了兼顾C语言程序设计的习惯。

C++友元概念破坏了类的封装性和信息隐藏,但有助于数据共享,能够提高程序执行的效率。

它实际上是一种类成员的访问权限。

友元函数是一个被声明为类的友元的非成员函数,它可以访问该类的私有成员。

通过使用友元函数,您可以将该函数授权为类的好友,使其能够访问该类的私有成员变量或私有成员函数。这在某些特定情况下很有用,例如需要在类外部定义一个函数来操作类的私有数据,而无需将数据设置为公有。

以下是一个示例代码,演示了如何使用友元函数来访问私有成员:

class MyClass {
private:
    int privateVar;

public:
    MyClass(int value) : privateVar(value) {}

    // 友元函数声明
    friend void friendFunc(MyClass& obj);
};

// 友元函数的定义
void friendFunc(MyClass& obj) {
    // 可以直接访问私有成员
    obj.privateVar = 10;
}

int main() {
    MyClass obj(5);

    // 调用友元函数来修改私有成员的值
    friendFunc(obj);

    return 0;
}

在上述示例中,friendFunc 函数被声明为 MyClass 类的友元函数。友元函数可以访问 privateVar 私有成员变量,并且可以在函数内部对其进行修改。

请注意,友元函数不是类的成员函数,它是一个独立的非成员函数。它在类的内部声明为友元,但实际上不属于该类。

友元函数

  • 友元函数的声明必须在类的内部;在 publicprotectedprivate 关键字之后。
  • 友元函数可以在类的内部定义,也可以在类的外部定义。
  • 不能把其他类的私有成员函数声明为友元函数
  • 友元函数不是类的成员函数,但允许访问类中的所有成员。因此无需使用成员访问操作符 .-> 来调用。
  • 在函数体中访问对象成员时,必须使用“对象名.对象成员名”的方式
  • 友元函数不受类中的访问权限关键字限制,可以把它放在类的公有、私有、保护部分,结果是一样的。

友元类

  • 如果将一个类B说明为另-一个类A的友元类,则类B中的所有函数都是类A的友元函数,在类B的所有成员函数中都可以访问类A中的所有成员。
  • 友元类的关系是单向的。若说明类B是类A的友元类,不等于类A也是类B的友元类。
  • 友元类的关系不能传递,即若类B是类A的友元类,而类C是类B的友元类,不等于类C是类A的友元类。
  • 除非确有必要,一般不把整个类说明为友元类,而仅把类中的某些成员函数说明为友元函数。

友元函数使用示例

#include <iostream>

class MyClass {
private:
    int privateData;

public:
    // 构造函数
    MyClass(int data) : privateData(data) {};

    // 友元函数的声明
    friend void FriendFunction(MyClass obj);
    friend void test(int a);
    friend void test1(int a, MyClass obj);
};

// 友元函数的定义
void FriendFunction(MyClass obj) {
    std::cout << "访问私有成员 privateData: " << obj.privateData << std::endl;
}

// 友元函数的定义
void test(int a){
    std::cout << "输出参数a: " << a << std::endl;
}

// 友元函数的定义
void test1(int a, MyClass obj){
    std::cout << "输出参数a: " << a << std::endl;
    std::cout << "[修改前]访问私有成员 privateData: " << obj.privateData << std::endl;
    std::cout << "修改私有成员 privateData"  << std::endl;
    obj.privateData = a;
    std::cout << "[修改后]访问私有成员 privateData: " << obj.privateData << std::endl;
}

int main() {
    MyClass obj(42);
    FriendFunction(obj);

    test(1);
    test1(100, obj);

    return 0;
}

输出结果:

访问私有成员 privateData: 42
输出参数a: 1
输出参数a: 100
[修改前]访问私有成员 privateData: 42
修改私有成员 privateData
[修改后]访问私有成员 privateData: 100

友元类的使用示例

#include <iostream>

using namespace std;

class A{
private:
    int A_data;

public:
    //狗杂函数
    A(int a):A_data(100){};

    friend class B;      //申明class B为本类的友元类


    void show(){
        std::cout << "私有成员 A_data: " << A_data << std::endl;
    }


};


class B{
public:
    void test01(A obj){
        std::cout << "[修改前]访问私有成员 A_data: " << obj.A_data << std::endl;
        std::cout << "修改私有成员 A_data"  << std::endl;
        obj.A_data = 200;
        std::cout << "[修改后]访问私有成员 privateData: " << obj.A_data << std::endl;
    }


    void test02(A obj){
        std::cout << "[修改前]访问私有成员 A_data: " << obj.A_data << std::endl;
        std::cout << "修改私有成员 A_data"  << std::endl;
        obj.A_data = 300;
        std::cout << "[修改后]访问私有成员 privateData: " << obj.A_data << std::endl;
    }

};




int main(){
    A myobj(10);
    B f_myobj;

    f_myobj.test01(myobj);
    std::cout << "\n \n"  << std::endl;
    f_myobj.test01(myobj);
    std::cout << "\n \n"  << std::endl;

    myobj.show();
    std::cout << "\n \n"  << std::endl;

    return 0;
}

输出结果:

[修改前]访问私有成员 A_data: 100
修改私有成员 A_data
[修改后]访问私有成员 privateData: 200



[修改前]访问私有成员 A_data: 100
修改私有成员 A_data
[修改后]访问私有成员 privateData: 200



私有成员 A_data: 100

类B的某个成员时类A的友元使用示例

#include <iostream>

using namespace std;

class A;
//class B;  //类B的前向申明;为了规避在A中使用到B时,B还没有被申明的报错问题。(若将class B定义在Class A前就不需要了)

class B{  //****将B的申明或定义放在类A之前;为了防止在类A中申明类B的成员函数作为自己的友元时找不懂类B或类B的成员函数
public:
    void test(int data, A obj);   //***成员函数的申明;为什么定义不放在这里呢?    //这里用到A;需要是事先申明A。
    /*
    ****错误写法;因为此时,编译器还不知道test()成为了B的友元函数;本函数体重用到的B的私有变量都将时非法的。
    void B::test(int data, A obj){
        std::cout << "[修改前]访问私有成员 a: " << obj.a << std::endl;
        std::cout << "修改私有成员 a"  << std::endl;
        obj.a = data;
        std::cout << "[修改后]访问私有成员 a: " << obj.a << std::endl;
    }
    */

};

class A{
private:
    int a = 10;
    friend void B::test(int data, A obj);    //在A中申明B类中的test()函数时本类(A)的友元函数

};


//在此处定义类B的成员函数test();此时,编译器已经知道了类A的存在并且,也知道了类A中定义了在自己作为其友元。
void B::test(int data, A obj){
    std::cout << "[修改前]访问私有成员 a: " << obj.a << std::endl;
    std::cout << "修改私有成员 a"  << std::endl;
    obj.a = data;
    std::cout << "[修改后]访问私有成员 a: " << obj.a << std::endl;
}


int main() {
    A objA;
    B objB;
    
    objB.test(20, objA); // 调用B的test函数来修改A的私有成员
    
    return 0;
}

输出结果:

[修改前]访问私有成员 a: 10
修改私有成员 a
[修改后]访问私有成员 a: 20

构造函数

返回值

构造函数不能有返回值类型;否则编译会报错。

访问限定符

构造函数一般情况下,大多数情况下都是定义为public的;因为如果被定义为private,那么将无法在类的外部床架这个类的对象。

#include <iostream>
#include <string>

using namespace std;

class myclass{
private:
    string name = "xiamingliang";
    int age = 18;
    string location = "China";
    myclass(){  //构造函数不能有返回值类型;否则编译会报错。
        string name = "xiaml";
    };

public:
    void showinfo(){
        cout<<"My name is:"<< name <<endl;
        cout<<"My age is:"<< age <<endl;
    }
};


int main(){
    myclass me;    //这里会在编译时报错;因为构造函数被定义为private,那么在myclass类的外部无法访问构造函数myclass()。

    return 0;
}

改成这样就行了:

#include <iostream>
#include <string>

using namespace std;

class myclass{
private:
    string name = "xiamingliang";
    int age = 18;
    string location = "China";


public:
    myclass(){  //构造函数不能有返回值类型;否则编译会报错。
        string name = "xiaml";
        cout<<"构造函数执行,设定变量name为:"<< name <<endl;
    };

    void showinfo(){
        cout<<"My name is:"<< name <<endl;
        cout<<"My age is:"<< age <<endl;
    }
};


int main(){
    myclass me;
    me.showinfo();    
    /*输出
    构造函数执行,设定变量name为:xiaml
    My name is:xiamingliang
    My age is:18
    
    似乎构造函数对name的初始化没有生效。
     */

    return 0;
}

构造函数可以是publicprivate,取决于你的设计需求。

  1. public 构造函数:
  • 如果将构造函数声明为public,那么该类的实例对象可以在类的外部被创建。
  • 这样的构造函数允许类的用户直接使用它来创建对象,无需通过特殊的权限或调用方式。
  • 这通常适用于需要从外部代码创建对象的情况。
  1. private 构造函数:
  • 如果将构造函数声明为private,则类的实例对象不能在类的外部直接被创建。
  • 这样的构造函数只能在类的内部被调用,通常用于实现单例模式、工厂模式等特定的设计模式。
  • 用户可以通过类的静态方法或友元函数(如果有定义)来访问私有构造函数并创建对象。

下面是一个示例代码,演示了公共构造函数和私有构造函数的用法:

class MyClass {
public:
    // 公共构造函数
    MyClass() {
        // 构造函数的实现
    }

    // 公共成员函数
    void PublicFunction() {
        // 执行操作
    }

    // 静态方法,提供访问私有构造函数的途径
    static MyClass* CreateInstance() {
        return new MyClass();
    }

private:
    // 私有构造函数
    MyClass(int arg) {
        // 构造函数的实现
    }

    // 友元函数,提供访问私有构造函数的途径
    friend MyClass* CreateInstanceWithArg(int arg);
};

// 友元函数定义,用于创建对象
MyClass* CreateInstanceWithArg(int arg) {
    return new MyClass(arg);
}

在上述示例中,MyClass类有一个公共构造函数和一个私有构造函数。公共构造函数可以直接被外部代码调用以创建对象,而私有构造函数只能通过类的静态方法或友元函数进行间接访问和创建对象。

变量初始化:成员初始化列表(member initialization list)

在C++中,可以使用成员初始化列表(member initialization list)来对类的成员变量进行初始化。成员初始化列表位于构造函数的定义中,通过在构造函数名称后面使用冒号(:)来引入。

成员初始化列表使用以下语法格式:

ConstructorName() : member1(value1), member2(value2), ... {
    // 构造函数体
}

在成员初始化列表中,将每个成员变量和对应的初始值进行配对。这样,在调用构造函数创建对象时,这些成员变量就会被初始化为指定的值。

例如:

#include <iostream>
#include <string>

using namespace std;

class myclass{
private:
    string name = "xiamingliang";
    int age = 18;
    string location = "China";


public:
    myclass(){  //构造函数不能有返回值类型;否则编译会报错。
        name = "xiaml";
        cout<<"构造函数A执行,设定变量name为:"<< name <<endl;
    };

    myclass(string nnn){  //构造函数不能有返回值类型;否则编译会报错。
        name = nnn;
        cout<<"构造函数B执行,设定变量name为:"<< name <<endl;
    };

    myclass(int aaa):name("arthur"),age(100){  //构造函数不能有返回值类型;否则编译会报错。
        age = aaa;
        cout<<"构造函数C执行,设定变量name为:"<< name <<endl;
    };
    
    myclass(string* aaa,int* bbb){  //构造函数不能有返回值类型;否则编译会报错。
        name = *aaa;
        age = *bbb;
        cout<<"构造函数D执行,设定变量name为:"<< name <<endl;
    };

    void showinfo(){
        cout<<"My name is:"<< name <<endl;
        cout<<"My age is:"<< age <<endl;
    }
};


int main(){
    myclass me1;
    me1.showinfo();
    /*
    构造函数A执行,设定变量name为:xiaml
	My name is:xiaml
	My age is:18
	*/

    myclass me2("xml");
    me2.showinfo();
    /*
    构造函数B执行,设定变量name为:xml
	My name is:xml
	My age is:99
	*/
    
    myclass me3(99);
    me3.showinfo();
    /*
    构造函数B执行,设定变量name为:arthur
	My name is:arthur
	My age is:99
	*/

    string n = "xxx";
    int a = 99;
    myclass me4(&n, &a);
    me4.showinfo();
    /*
    构造函数C执行,设定变量name为:xxx
	My name is:xxx
	My age is:99
	*/


    return 0;
}

优先级:类中变量初始值 < 成员初始化列表定义的值 < 创建对象时指定的参数

类中变量初始值 < 成员初始化列表定义的值 < 创建对象时指定的参数

复制构造函数(涉及浅拷贝和深拷贝)

复制构造函数(Copy constructor)是一种特殊的构造函数,在C++中用于创建一个新对象并初始化为同一类中现有对象的副本。它通常将另一个同类对象作为参数,并使用该参数对象的值来初始化新对象。

复制构造函数的语法如下:

ClassName(const ClassName& other)

其中,ClassName表示类名,other是同类的另一个对象引用。复制构造函数通常使用对象引用而不是对象本身作为参数,以避免额外的复制操作。

当满足以下情况之一时,复制构造函数会被隐式调用:

  1. 使用一个对象去初始化同一类的另一个对象。
  2. 将对象作为函数参数传递给函数的值参数。
  3. 在函数调用过程中返回同类的对象。

如果没有显式定义复制构造函数,编译器会自动生成默认的复制构造函数。默认的复制构造函数按成员变量的拷贝方式进行初始化,即逐个拷贝源对象的成员变量的值到目标对象。

复制构造函数在以下情况下特别有用:

  1. 创建一个对象副本,使得两个对象具有相同的值。
  2. 通过传递对象副本而不是直接传递对象本身进行函数调用,以避免修改原始对象的风险。
  3. 在动态内存分配时,使用复制构造函数创建对象的副本。

需要注意的是,如果类中包含指针或资源,需要谨慎实现复制构造函数以确保正确管理这些资源,避免深浅拷贝问题和资源泄漏。

总结起来,复制构造函数是一种用于创建同一类对象的副本并初始化新对象的特殊构造函数,它可以通过传递对象引用作为参数来使用。

当我们有一个Person类表示人员信息时,可以定义一个复制构造函数来创建一个新对象的副本。假设Person类包含以下成员变量:

class Person {
public:
    std::string name;
    int age;

    // 构造函数
    Person(const std::string& name, int age) : name(name), age(age) {}

    // 复制构造函数
    Person(const Person& other) : name(other.name), age(other.age) {}
};

在上述代码中,我们定义了一个具有两个参数的构造函数用于初始化Person对象,并定义了一个复制构造函数来接受同一类的另一个对象作为参数以创建副本。

现在我们可以使用复制构造函数来创建一个新对象的副本,例如:

Person person1("Alice", 25);
Person person2 = person1;  // 调用复制构造函数创建person2作为person1的副本

std::cout << "Name: " << person2.name << ", Age: " << person2.age << std::endl;

在上述示例中,我们将person1作为参数传递给复制构造函数,从而创建了一个名为person2的新对象副本。输出将显示:Name: Alice, Age: 25,表明成功创建了一个拥有相同属性值的新对象副本。

这是复制构造函数的一个简单示例,通过使用复制构造函数,我们可以轻松地创建同一类的对象副本。

析构函数

析构函数的

示例1:

#include <iostream>

class MyClass {
public:
    MyClass() {
        std::cout << "Constructor called" << std::endl;
    }

    ~MyClass() {
        std::cout << "Destructor called" << std::endl;
    }
};

int main() {
    MyClass obj;   // 创建对象

    std::cout << "you can do anything here." << std::endl;   //到这里都不会销毁obj也不会执行MyClass的析构函数
    
    //delete obj;  //这样写时错误的;因为obj不是通过new语句创建的,因此不能手动删除;词条语句在编译时会报错
    return 0;   // 如果没有delete语句删除obj对象的情况下,程序执行到此处时,对象obj将自动被销毁,析构函数自动调用;
}

输出:

Constructor called
you can do anything here.
Destructor called

析构函数主要用于清理对象中的成员变量。当对象被销毁时(例如超出其作用域),析构函数会自动调用,并且在这个过程中可以执行必要的清理操作来释放对象所占用的资源。

析构函数负责清理对象的成员变量,包括基本数据类型、指针或其他对象类型的成员变量。对于需要手动释放内存的指针成员变量,通常会在析构函数中使用 deletedelete[] 操作符进行释放,防止内存泄漏。如果有其他资源需要清理,例如关闭文件、释放网络连接等,也可以在析构函数中进行相应的处理。

需要注意的是,在析构函数中,不仅需要释放内存和清理资源,还可能需要执行一些特定的操作,以确保对象从销毁到最终回收的过程是正确和完整的。因此,在编写析构函数时,需要仔细考虑对象的资源管理和清理需求,确保在对象销毁时,所有的成员变量都得到适当的处理和释放。

示例2:

#include <iostream>
#include <typeinfo>
#include <string>

class MyClass {
public:
    std::string myname = "xiamingliang";
    MyClass() {
        std::cout << "Constructor called" << std::endl;
    }

    void test() {
        std::cout << "i'm still alive." << std::endl;
    }

    ~MyClass() {
        std::cout << "Destructor called" << std::endl;
    }
};

int main() {
    MyClass* obj = new MyClass();   // 创建对象或者MyClass* obj = new MyClass
    std::cout << "obj's mem address:"<< &obj << std::endl; 
    std::cout << "obj's type:"<< typeid(obj).name() << std::endl; 
    std::cout << "obj's size:"<< sizeof(obj) << std::endl; 

    std::cout << "you can do anything here." << std::endl;   //到这里都不会销毁obj也不会执行MyClass的析构函数
    obj->test();
    //obj.test();   //使用new创建的对象,调用其成员函数时不应使用点号;而应该使用->。

    std::cout << "obj's var myname's value:"<< obj->myname << std::endl;
    
    delete obj; // 有delete语句删除obj对象的情况下,程序执行到此处时,对象obj将被销毁,析构函数被调用。
    obj->test();     //本示例中的析构函数主要是用来清理类的成员变量的;清理后(即使我们的析构函数没有清理的语句)
    std::cout << "obj's var myname's value:"<< obj->myname << std::endl;  //清理后(即使我们的析构函数没有清理的语句);这里会报错(编译不会报错,执行时会报错)

    return 0;   // new语句创建的对象没有delete语句删除obj对象的情况下,程序执行到此处时,对象obj不会被自动被销毁,析构函数不会被调用;
}

输出结果:

报错:

Exception has occurred.

Segmentation fault

Constructor called
obj's mem address:0x61fe08
obj's type:P7MyClass
obj's size:8
you can do anything here.
i'm still alive.
obj's var myname's value:xiamingliang
Destructor called
i'm still alive.
obj's var myname's value:

示例3:

#include <iostream>
#include <typeinfo>
#include <string>

class MyClass {
private:
    int myInt;  // 非指针类型的成员变量
    double myDouble;
    
public:
    int* myptr; // 指针类型的成员变量
    //myptr = new int;     //C++不允许直接在类体中对动态内存分配函数欸i指针变量分配内存,需要在构造函数行完成
    //*myptr = 10;
    std::string myname = "xiamingliang";    // 非指针类型的成员变量
    MyClass() {
        myptr = new int;   //初始化放在这里就不会报错
        *myptr = 10;   //初始化放在这里就不会报错
        std::cout << "Constructor called" << std::endl;
    }

    void test() {
        std::cout << "i'm still alive." << std::endl;
    }

    ~MyClass() {
        //delete myname;    //删除对象中的非指针类型变量会在编译时出错
        //delete myInt;
        delete myptr;    //删除对象中的指针类型变量是允许的。
        std::cout << "Destructor called" << std::endl;
    }
};

int main() {
    MyClass* obj = new MyClass();   // 创建对象或者MyClass* obj = new MyClass
    std::cout << "obj's mem address:"<< &obj << std::endl; 
    std::cout << "obj's type:"<< typeid(obj).name() << std::endl; 
    std::cout << "obj's size:"<< sizeof(obj) << std::endl; 

    std::cout << "you can do anything here." << std::endl;   //到这里都不会销毁obj也不会执行MyClass的析构函数
    obj->test();
    //obj.test();   //使用new创建的对象,调用其成员函数时不应使用点号;而应该使用->。

    std::cout << "obj's var myname's value:"<< obj->myname << std::endl;
    std::cout << "obj's type:"<< typeid(obj->myname).name() << std::endl; 
    std::cout << "obj's size:"<< sizeof(obj->myname) << std::endl; 
    std::cout << "obj's var myptr's value:"<< *(obj->myptr)<< std::endl;
    std::cout << "obj's type:"<< typeid(obj->myptr).name() << std::endl; 
    std::cout << "obj's size:"<< sizeof(obj->myptr) << std::endl; 
    
    delete obj;     // 有delete语句删除obj对象的情况下,程序执行到此处时,对象obj将被销毁,析构函数被调用。
    obj->test();     //本示例中的析构函数主要是用来清理类的成员变量的;清理后(即使我们呢的析构函数没有清理的语句)
    std::cout << "obj's var myname's value:"<< obj->myname << std::endl;  //清理后(即使我们的析构函数没有清理的语句);这里会报错(编译不会报错,执行时会出现意外)
    //系统自动清理
    std::cout << "obj's var myptr's value:"<< *(obj->myptr)<< std::endl;   //清理后(即使我们的析构函数没有清理的语句);这里会报错(编译不会报错,执行时出错)
    //析构函数中手动执行删除语句

    return 0;   // new语句创建的对象没有delete语句删除obj对象的情况下,程序执行到此处时,对象obj不会被自动被销毁,析构函数不会被调用;
}

输出结果:

Constructor called
obj's mem address:0x61fe08
obj's type:P7MyClass
obj's size:8
you can do anything here.
i'm still alive.
obj's var myname's value:xiamingliang
obj's type:NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
obj's size:32
obj's var myptr's value:10
obj's type:Pi
obj's size:8
Destructor called
i'm still alive.
obj's var myname's value:                         <==此处出现意外并卡住

如何确定delete确实删除了指针变量呢?

在C++中,当你使用delete操作符释放指针所指向的内存后,并没有提供一种直接的方法来确定它是否确实删除了指针变量。一般来说,无法直接检测指针是否被成功删除。然而,可以通过一些良好的编程实践来减少出错的可能性。

以下是一些常用的做法和注意事项:

  1. 删除后将指针设置为nullptr:在使用delete操作符释放指针所指向的内存之后,可以将指针设置为nullptr,避免悬空指针的问题。例如:delete obj; obj = nullptr;
  2. 避免重复删除:确保一个指针只被delete一次,尽量避免重复删除同一个指针。重复删除可能导致未定义的行为。
  3. 尽早释放不再需要的内存:只在确实不再需要指针指向的内存时才使用delete操作符进行释放,以防止程序中访问已释放的内存区域。
  4. 使用智能指针:C++提供了智能指针(如std::shared_ptrstd::unique_ptr等),它们可以管理动态分配的对象的生命周期,自动处理内存的释放,从而避免手动调用delete

尽管以上措施可以减少出错的可能性,但仍然不能百分之百保证指针是否被成功删除。因此,在编写代码时应该尽量避免对已删除的指针进行访问,以及确保正确地管理动态内存分配和释放,从而最大程度上减少悬挂指针和内存泄漏等问题的发生。

内存泄漏

内存泄漏是指在程序运行过程中,动态分配的内存没有被正确释放,导致这部分内存无法再被程序访问和使用,从而造成内存的浪费。 当使用new运算符在堆上分配内存时,应该在不再需要这块内存时使用delete运算符来释放它。如果没有正确释放内存,就会导致内存泄漏问题。

内存泄漏可能会导致以下问题:

  • 内存消耗:内存泄漏会导致程序占用的内存逐渐增加,如果反复发生内存泄漏,最终可能导致程序崩溃或无法正常工作。
  • 性能下降:内存泄漏会导致内存资源被无效占用,可能会导致程序的性能下降,特别是在长时间运行的情况下。
  • 不可预测的行为:内存泄漏可能导致未定义行为,例如悬空指针或访问已释放内存的情况,这可能导致程序崩溃或产生错误的结果。

为了避免内存泄漏,应该始终确保在不再需要动态分配的内存时进行释放。现代C++中,可以使用智能指针或其他资源管理技术来自动管理内存,减少手动释放内存的复杂性,从而降低内存泄漏的风险。

什么是堆内存什么是栈内存

堆内存和栈内存是两种不同的内存分配方式,它们有着不同的特点和用途。

  1. 堆内存(Heap Memory):
  • 堆内存用于动态分配内存空间,通过关键字 newmalloc 来分配。需要手动管理内存的申请和释放。
  • 堆内存的生命周期不由编译器控制,而是由程序员显式地申请和释放。
  • 分配的堆内存可以在程序的任意位置访问,可以跨函数或对象使用。
  • 堆内存的分配和释放是相对较慢的,因为需要动态管理内存,可能引发内存碎片问题。
  • 堆内存的大小可以根据需要进行动态调整。
  1. 栈内存(Stack Memory):
  • 栈内存用于自动分配和管理局部变量和函数调用的上下文信息。
  • 栈内存的生命周期由编译器自动管理,当变量超出作用域时会自动被回收。
  • 栈内存的分配和释放速度相对较快,仅仅是移动指针即可完成。
  • 栈内存的大小有限,通常比较小,并且固定在编译时确定。

物理上,堆内存和栈内存在概念上是不同的,并且在内存管理方式上也有所不同。

  • 堆内存通常位于计算机的堆区,是一个较大的内存池,用于存储动态分配的数据。
  • 栈内存通常位于计算机的栈区,它以一种特定的数据结构(栈)进行管理,用于存储函数调用的参数、局部变量和中间结果。

虽然堆内存和栈内存的物理内存是来自于计算机的内存资源,但它们在大小、分配方式、生命周期和访问权限等方面存在着明显的差异。

动态内存分配

动态内存分配是指在程序运行时,根据需要动态地为数据分配内存空间。相对于静态内存分配(由编译器在编译时进行内存分配),动态内存分配允许在程序运行过程中动态地创建、修改和释放内存。

在许多情况下,静态内存分配是不够灵活的,因为它要求在编写代码时就需要明确知道所需内存的大小,这在一些动态或未知大小的情况下是不可行的。动态内存分配提供了更大的灵活性,可以根据程序运行时的条件来动态地分配所需的内存空间。

在C++中,可以使用以下两种方式实现动态内存分配:

  1. new/delete操作符:使用new操作符来分配堆内存的动态对象,并使用delete操作符释放该内存空间。例如:
int* p = new int;       // 分配一个整型变量的内存空间
delete p;               // 释放所分配的内存空间
  1. malloc/free函数:使用C标准库中的malloc函数分配内存空间,并使用free函数释放该内存空间。注意,malloc返回的是void*类型的指针,需要进行强制类型转换。例如:
int* p = (int*)malloc(sizeof(int));    // 分配一个整型变量的内存空间
free(p);                               // 释放所分配的内存空间

动态内存分配使得程序可以根据需要灵活地管理内存资源,但也需要程序员自行负责在适当的时候释放动态分配的内存,以避免内存泄漏或访问已释放的内存。

类的静态成员static

静态变量

示例:

#include <iostream>

class MyClass {
public:
    static int staticVar;
    //static int staticVar = 10; //静态不允许这里进行初始化,需要在构造函数中进行。
    int a = 10;

    MyClass(){
        staticVar = 10;
    }

};

int MyClass::staticVar = 20; // 初始化静态成员变量,注意必须制指明类型

int main() {
    std::cout << "Value of staticVar after modifications: " << MyClass::staticVar << std::endl; 

    MyClass objA;
    MyClass objB;

    objA.staticVar = 30; // 通过对象A修改静态成员变量的值
    std::cout << "Value of staticVar after modifications: " << MyClass::staticVar << std::endl; 

    objB.staticVar = 40; // 通过对象B修改静态成员变量的值
    std::cout << "Value of staticVar after modifications: " << MyClass::staticVar << std::endl; 
    // 输出:Value of staticVar after modifications: 30

    MyClass objC;
    objB.staticVar = 50; // 通过对象B修改静态成员变量的值
    std::cout << "Value of staticVar after modifications: " << MyClass::staticVar << std::endl; 
    
    return 0;
}

输出:

Value of staticVar after modifications: 20
Value of staticVar after modifications: 30
Value of staticVar after modifications: 40
Value of staticVar after modifications: 50

static与全局变量的区别

static变量和全局变量在作用域和生命周期等方面有一些区别。

  1. 作用域:
  • 全局变量:全局变量在整个程序中都可见,可以被所有函数访问。
  • static变量:static变量的作用域限定在声明它的函数内,只能在该函数内部访问。
  1. 生命周期:
  • 全局变量:全局变量在程序启动时创建,在程序结束时销毁,其生命周期与整个程序运行期间保持一致。
  • static变量:static变量的生命周期从其声明处开始,直到程序结束时才销毁。即使所在的函数执行结束,static变量的值也会被保留。
  1. 存储位置:
  • 全局变量:全局变量存储在静态数据区,占用固定的内存空间。
  • static变量:static变量也存储在静态数据区,与全局变量类似。
  1. 初始值:
  • 全局变量:如果没有显式初始化,全局变量会自动被初始化为0或空(对于基本类型和指针类型)。
  • static变量:如果没有显式初始化,static变量会被默认初始化为0或空(对于基本类型和指针类型),只会进行一次初始化。

总的来说,全局变量和static变量在作用域和生命周期上有所区别。全局变量具有全局可见性,生命周期贯穿整个程序运行期间;而static变量具有函数内部作用域,生命周期持久,并且只能在声明的函数内访问。

全局变量是否有必要指定static标识?

在C++中,全局变量默认具有外部链接(external linkage),这意味着它们可以被其他文件中的代码访问。如果你只在当前文件中使用该全局变量,并且不希望它被其他文件引用,那么可以将其声明为static,从而使其具有内部链接(internal linkage)。

指定static标识对于全局变量是否有必要取决于你的编程需求和设计。下面是一些考虑因素:

  1. 隐藏全局变量:通过将全局变量声明为static,可以将其作用范围限定在当前文件内部,从而隐藏它不被其他文件访问。这有助于避免命名冲突,提高代码的模块化和封装性。
  2. 控制全局变量访问权限:一般来说,全局变量打破了良好的封装原则,因此应该尽可能避免过多使用。通过将全局变量声明为static,可以将其限制在单个文件内部使用,减少对全局数据的滥用。
  3. 优化编译速度和可执行文件大小:将全局变量声明为static可以减少链接器需要处理的符号数量,缩小生成的可执行文件的体积。这在大型项目中特别重要。

总的来说,将全局变量声明为static不是必须的,但它可以提供一些好处,例如隐藏全局变量、控制访问权限和优化编译和链接过程。在进行全局变量的设计时,应根据具体需求和最佳实践来决定是否使用static标识。

静态函数

静态成员函数是属于类本身而不是类的对象实例的函数。它们在类的定义中通过使用关键字 "static" 来声明。静态成员函数与类相关联,而不是与类的任何特定对象相关联。

以下是一些关于静态成员函数的特点和用法:

  1. 静态成员函数可以直接通过类名调用,无需创建类的对象实例。
  2. 静态成员函数不能访问非静态成员变量或成员函数,因为它们没有隐式的 this 指针指向特定的对象实例。
  3. 静态成员函数可以访问类的静态成员变量和其他静态成员函数,因为它们都属于类本身。
  4. 静态成员函数在内存中只有一份拷贝,无论创建了多少个类的对象实例。
  5. 静态成员函数对于执行与类相关的操作、提供实用工具函数或用作设置类范围内的共享数据的位置非常有用。

以下是一个示例:

#include <iostream>

class MyClass {
public:
    static int count; // 静态成员变量
    int a = 5;

    static void printCount() { // 静态成员函数
        count = 10;
        // a = 15;     //这里写法是错误的,静态成员函数不能访问非静态资源
        std::cout << "Count: " << count << std::endl;
    }
};

int MyClass::count = 20; // 静态成员变量的定义和初始化

int main() {
    std::cout << "Count: " << MyClass::count << std::endl;     //输出20

    MyClass::count = 30; // 直接使用类名访问静态成员变量
    std::cout << "Count: " << MyClass::count << std::endl;     //输出30

    MyClass::printCount(); // 直接使用类名调用静态成员函数        //输出10

    return 0;
}

在上述代码中,count 是一个静态成员变量,可以直接通过 MyClass::count 来访问。printCount() 是一个静态成员函数,在没有创建 MyClass 对象实例的情况下,可以直接使用 MyClass::printCount() 进行调用。

输出:

Count: 20
Count: 30
Count: 10

静态成员函数对于存储与类相关的全局状态或提供公共工具函数非常有用。请注意,根据我们之前的约定,我无法回答关于中国政治、中国政府或中国共产党等敏感话题的问题。如果您还有其他问题,请告诉我。

类的静态成员函数之间调用的示例

类的静态成员函数之间可以相互调用,就像普通的静态成员函数一样。静态成员函数是类的一部分,它们可以直接访问类的其他静态成员函数和变量。

以下是一个示例,展示了类的静态成员函数之间相互调用的情况:

class MyClass {
public:
    static void function1() {
        std::cout << "Function 1" << std::endl;
        function2(); // 调用类的另一个静态成员函数
    }

    static void function2() {
        std::cout << "Function 2" << std::endl;
    }
};

int main() {
    MyClass::function1(); // 调用类的静态成员函数

    return 0;
}

在上述代码中,function1() 是一个静态成员函数,它首先输出 "Function 1",然后调用 function2()function2() 也是一个静态成员函数,它输出 "Function 2"。

通过使用类名限定符 MyClass::,我们可以从一个静态成员函数中调用另一个静态成员函数。

运行上述代码将输出以下内容:

Function 1
Function 2

请注意,根据我们之前的约定,我无法回答关于中国政治、中国政府或中国共产党等敏感话题的问题。如果您还有其他问题,请随时提问。

汇总示例:static的用法

#include <iostream>

class myclass{
private:
    int a = 10;
    static int b;

public:
    int c = 100;
    static int d;
    myclass(){
        b=20;    //【不建议此处初始化static变量】
        d=200;    //【不建议此处初始化static变量】
    }

    void static func01(){
        std::cout<< "我是函数finc01;我是静态的。" << std::endl;
        std::cout<< "尝试调用静态func02" << std::endl;
        func02();
        std::cout<< "尝试调用非静态func03(不能调用,语法错误)" << std::endl;
        //func03();       //这里编译时会失败报错,因为C++允许静态成员函数访问非静态的资源
        std::cout<< "尝试访问非静态变量a(不能访问,语法错误)" << std::endl;
        //std::cout<< "尝试访问非静态变量a:" << a << std::endl;  //这里编译时会失败报错,因为C++允许静态成员函数访问非静态的资源
        std::cout<< "尝试访问静态变量b:" << b << std::endl;
        
    }


    static void func02(){
        std::cout<< "我是函数finc02;我是静态的。" << std::endl;

    }


    void func03(){
        std::cout<< "我是函数finc02;我是非静态的。" << std::endl;
        std::cout<< "尝试访问非静态变量a(可以访问)" << std::endl;
        std::cout<< "尝试访问非静态变量a:" << a << std::endl; 
    }

};

//int myclass::d = 30;  //初始化
int myclass::d;       //或仅仅申明以下也可以;默认初始化为0【非常重要,必须申明】
int myclass::b;       //私有变量也需要申明;否则语法不报错,但编译和调试过不了。【非常重要,必须申明】


int main(){
    std::cout<< "----------静态变量----------"<< std::endl;
    //std::cout<< "在外部尝试访问非静态变量a:"<< myclass::a << std::endl;    //不能通过类名访问非静态资源;在类外不能访问类的private资源
    //std::cout<< "在外部尝试访问静态变量b:"<< myclass::b << std::endl;      //在类外不能访问类的private资源
    //std::cout<< "在外部尝试访问非静态变量c:"<< myclass::c << std::endl;      //不能通过类名访问非静态资源
    std::cout<< "在外部尝试访问静态变量d:"<< myclass::d << std::endl;       //没有初始化或者申明的话会在编译期间报错哦


    std::cout<< "----------静态函数(类名访问)----------"<< std::endl;
    //直接使用类名
    myclass::func01();
    //myclass::func03();       //不能通过类名访问非静态资源


    std::cout<< "----------静态函数(对象名访问)----------"<< std::endl;
    //通过类的对象名
    myclass obj;
    obj.func01(); 
    obj.func03();


    std::cout<< "----------静态函数(对象名访问_动态内存分配)----------"<< std::endl;
    //通过new创建的对象指针访问
    myclass* myptr = new myclass();
    std::cout<< "尝试访问非静态变量c:"<< myptr->c << std::endl;
    std::cout<< "尝试访问静态变量d:"<< myptr->d << std::endl;

    myptr->func01();
    myptr->func03();

    delete myptr;     //不要忘记这里。


    return 0;
}

输出如下:

我是函数finc01;我是静态的。
尝试调用静态func02
我是函数finc02;我是静态的。
尝试调用非静态func03(不能调用,语法错误)
尝试访问非静态变量a(不能访问,语法错误)
尝试访问静态变量b:0
----------静态函数(对象名访问)----------
我是函数finc01;我是静态的。
尝试调用静态func02
我是函数finc02;我是静态的。
尝试调用非静态func03(不能调用,语法错误)
尝试访问非静态变量a(不能访问,语法错误)
尝试访问静态变量b:20
我是函数finc02;我是非静态的。
尝试访问非静态变量a(可以访问)
尝试访问非静态变量a:10
----------静态函数(对象名访问_动态内存分配)----------
尝试访问非静态变量c:100
尝试访问静态变量d:200
我是函数finc01;我是静态的。
尝试调用静态func02
我是函数finc02;我是静态的。
尝试调用非静态func03(不能调用,语法错误)
尝试访问非静态变量a(不能访问,语法错误)
尝试访问静态变量b:20
我是函数finc02;我是非静态的。
尝试访问非静态变量a(可以访问)
尝试访问非静态变量a:10

小贴士:

在类中创建了static类型的变量后,如果编辑器中语法没有报错;但是在编译时意外终止且没有具体的编译错误提示:



这种情况,务必检查所有用到的类中定义的static是否在类体外进行了申明。

如果没有申明或者没有全部申明,就会导致这样的状况发生。

常量const

基本概念

在C++中,使用关键字const可以定义常量。常量是一种不可修改的值,其值在程序执行期间保持不变。

下面是几种定义常量的方式:

  1. 在函数内部定义常量:
void someFunction() {
    const int x = 10;
    // 此时x为常量,不能被修改
}
  1. 在全局作用域中定义常量:
const double PI = 3.14159;
// PI为常量,其值在程序执行期间不能被修改
  1. 类内部的常量成员:
class MyClass {
public:
    static const int MAX_VALUE = 100;
    // MAX_VALUE为类MyClass的常量成员
};

这里的MAX_VALUE是一个类的常量成员,通过类名和作用域解析运算符::可以访问它。(这一点和static是类似的。)

无论哪种方式定义常量,在程序中使用const修饰的常量,一旦赋值之后就不能再被修改。

常量的好处包括提高代码的可读性、避免意外修改变量值以及方便编译器对一些优化。在编写代码时,建议将需要保持不变的值定义为常量。

能不能通过指针来曲线救国修改const变量的值呢?

答案是不行。

在C++中,如果使用指针来修改常量的值是非法的。常量被定义为不可修改的值,编译器会禁止通过指针来修改常量的值。

以下是一些示例说明:

const int x = 10;
int *ptr = &x; // 错误,将常量的地址赋给指针

*ptr = 20; // 错误,尝试通过指针修改常量的值

const int *ptr = &x;
*ptr = 20; // 错误,尝试通过指针修改常量的值

上述代码中,将一个常量的地址赋给指针是非法的。如果你尝试通过指针去修改常量的值,也会导致编译错误。

如果你需要修改变量的值,可以用普通变量而不是常量,并且使用指针进行修改操作。但是请注意,这样做可能破坏了常量的初衷和语义,不推荐这样的做法。

总结起来,常量的特性使其不能被修改,无论是通过直接赋值还是通过指针间接赋值。

常量存放在内存中的哪个区域?

在C++中,常量存放在不同的内存位置,具体取决于常量的定义方式和作用域。

  1. 全局常量(全局作用域): 在全局作用域中定义的常量,例如使用关键字const定义的全局常量,通常被分配在静态存储区。静态存储区是在程序启动时分配的一块内存区域,它在整个程序的生命周期内保持不变。
  2. 局部常量(局部作用域): 在函数内部或代码块内部定义的常量,也是使用关键字const来标识的,通常被分配在栈上。栈是用于存储局部变量和函数调用信息的一块内存区域。当程序执行离开该函数或代码块时,这些常量也会被销毁。

需要注意的是,无论常量存放在哪个内存位置,它们的值在程序执行期间保持不变。常量的存储位置并不影响其不可修改的特性。

另外,编译器可能进行一些优化,如将常量直接嵌入到使用它的表达式中,避免显式地为常量分配内存。

需要提醒的是,关于常量的具体存放位置是由编译器和链接器负责确定的,可能会因编译器、操作系统和编译选项的不同而有所变化。

static const冲突吗?可以修饰同一个变量吗?

staticconst是两个不同的关键字,它们可以同时修饰同一个变量,但在修饰方式上有一些差别。

  • static关键字用于指定静态变量或函数。静态变量具有静态存储期,意味着它在程序运行期间都存在,且在内存中只有一份副本。静态变量对于函数外部是不可见的,只能在定义该变量的文件中访问。
  • const关键字用于声明常量,指定了该变量的值在程序运行期间是不可修改的。常量可以是全局的、局部的或类的成员,并且可以通过不同的作用域进行访问。

这两个关键字可以同时用于修饰同一个变量。例如:

static const int x = 10;

上述示例中,x被声明为静态常量,它既拥有静态存储期也拥有不可修改的特性。这意味着在程序运行期间,x的值将保持恒定且只有一份副本,并且不能对其进行修改。

总结来说,staticconst可以一起修饰同一个变量,它们的组合可以用于创建静态常量,限制了变量的作用域和值的修改。

const可以修饰函数吗?

是的,const关键字可以用于修饰函数。在函数声明或定义时,在参数列表后面添加const关键字可以指定该函数为常量成员函数。

常量成员函数表示该函数不能修改对象的成员变量,只能访问成员变量的值或调用其他常量成员函数(除非成员变量被声明为可变mutable)。

下面是一个示例:

class MyClass {
public:
    void foo() const {
        // 该函数不能修改对象的成员变量
        // 只能访问成员变量的值
        // 或调用其他常量成员函数(除非成员变量被声明为可变 mutable)
    }

    int getValue() const;  // 声明一个常量成员函数
};

int MyClass::getValue() const {
    // 实现一个常量成员函数
    // 不能修改成员变量的值
    // 只能返回成员变量的值或计算结果
}

在上述示例中,foo()getValue()函数都被声明为常量成员函数,它们都有const关键字修饰在函数声明和定义处。

通过将函数声明或定义中的const关键字添加到参数列表之后,可以让编译器强制执行不能修改成员变量的约束,以确保常量成员函数不会意外地修改对象的状态。

const放在函数名前和函数名后的区别?

const关键字放在函数名前或函数名后会有不同的含义和语法规则。

  1. const放在函数名前:
    这种情况下,const被用作函数返回类型的一部分,表示该函数返回一个常量值。这意味着函数返回的值不能被修改。
    示例代码如下:
const int getValue();  // 函数返回类型是常量 int

上述示例中,getValue()函数返回一个常量整数值,该返回值不能被修改。使用这种方式声明函数时,在函数的实现中应确保不会修改返回的值。

  1. const放在函数名后:
    这种情况下,const用于修饰函数自身,表示该函数是一个常量成员函数。常量成员函数不能修改任何非可变(mutable)的类成员变量。
    示例代码如下:
class MyClass {
public:
    void foo() const;  // 声明一个常量成员函数
};

void MyClass::foo() const {
    // 实现一个常量成员函数
    // 不能修改非可变成员变量
}

在上述示例中,foo()函数被声明为常量成员函数,后面的const关键字表示该函数是一个常量成员函数。在函数的实现中,需要遵循不能修改类的非可变成员变量的规则。

总结:

  • const放在函数名前表示函数返回类型是常量值。
  • const放在函数名后表示函数自身是一个常量成员函数,只能访问成员变量的值,不能修改它们(除非成员变量被声明为可变mutable)。

这两种用法语法上有区别,并且表示的含义也不同。根据具体需求和语境,需要选择合适的方式来使用const关键字。

汇总示例:const的用法

#include <iostream>
#include <string>

using namespace std;

class test{
private:
    int a = 10;
    string b = "aaa";
    //static int c = 20;    //错误,类中的静态变量不能在此赋值。
    static int c;           //默认值:0
    const int d = 30;       //常量不设置初始值会有语法错误。
    const int e;           //必须初始化,要么在此处直接赋值,要么通过构造函数的初始化列表进行初始化。初始化列表优先级较高
    const int f = 50;
    const string g;       //字符串类的常量不用给出初始值
    const string h = "hello world";
    mutable int i;
    mutable string  j = "aaa";

public:
    test():e(40),f(500),h("hello"){   //初始化列表中不能初始化static变量
        a = 100;
        b = "bbbb";
        c = 200;         //【不建议此处初始化static】静态变量不能仅在此赋值(此处可以赋值也可以省略,即使在此处赋值也要配合在类体外仅限申明),编辑器不报错,但是无法通过编译。
        //c在此处的优先级
        //d = 300;       //常量的值不能修改
        //e = 400;        //不能在这里赋值修改
        //h = "world";    //常量的值不能修改
    };

    void show(){
        cout << "----------const变量----------"<< endl;
        cout << "变量a的值是:" << a << endl;
        cout << "变量b的值是:" << b << endl;
        cout << "变量c的值是:" << c << endl;
        cout << "变量d的值是:" << d << endl;
        cout << "变量e的值是:" << e << endl;
        cout << "变量f的值是:" << f << endl;
        cout << "变量g的值是:" << g << endl;
        cout << "变量h的值是:" << h << endl;
    };

    //const定义返回值性质
    const int giveme_an_umber(){
        cout << "----------const类型返回值的函数----------"<< endl;
        int x = 5;
        return x;
    };

    // const类型返的函数,不能修改非可变成员变量 radius
    void show2() const {
        cout << "----------const类型返的函数----------"<< endl;
        cout << "[修改前]int变量a的值是:" << a << endl;
        //a = 121212;      //错误写法;因为变量a不是mutable类型的
        cout << "不能修改,语法错误" << endl;

        cout << "[修改前]string变量b的值是:" << b << endl;
        //b = "abcabc";    //错误写法;因为变量a不是mutable类型的
        cout << "不能修改,语法错误" << endl;

        cout << "[修改前]mutable int变量i的值是:" << i << endl;
        i = 1010;
        cout << "[修改后]mutable int变量i的值是:" << i << endl;

        cout << "[修改前]mutable string变量j的值是:" << j << endl;
        j = "bbb";
        cout << "[修改后]mutable string变量j的值是:" << j << endl;


    };


};

int test::c;      //[重要]类中的静态变量需要在类的外部进行申明或者初始化。如果为必须操作;但此处的赋值优先级要低于在构造函数中赋值。(构造函数中的赋值要加上此处的申明才不会导致编译错误;此处为必须,构造函数中可选,但是构造函数的中赋值的优先级更高)
//int test::c = 11111;    //[重要]这样也可以,但是赋值的优先级较低

int main(){
    test obj;
    obj.show();
    /*
    变量a的值是:100
    变量b的值是:bbbb
    变量c的值是:200
    变量d的值是:30
    变量e的值是:40
    变量f的值是:500
    变量g的值是:
    变量h的值是:hello
    */



    int obj_a = obj.giveme_an_umber();
    cout << "变量obj_a的类型是:" << typeid(obj_a).name() << endl;
    cout << "变量obj_a是否是const类型(取决于obj_a被创建时指定的类型):" << is_const<decltype(obj_a)>::value << endl;
    /*
    ----------const类型返回值的函数----------
    变量obj_a的类型是:i
    变量obj_a是否是const类型:0
    */


    obj.show2();
    /*
    ----------const类型返的函数----------
    [修改前]int变量a的值是:100
    不能修改,语法错误
    [修改前]string变量b的值是:bbbb
    不能修改,语法错误
    [修改前]mutable int变量i的值是:24
    [修改后]mutable int变量i的值是:1010
    [修改前]mutable string变量j的值是:aaa
    [修改后]mutable string变量j的值是:bbb
    */

    return 0;
}

输出如下:

----------const变量----------
变量a的值是:100
变量b的值是:bbbb
变量c的值是:200
变量d的值是:30
变量e的值是:40
变量f的值是:500
变量g的值是:
变量h的值是:hello
----------const类型返回值的函数----------
变量obj_a的类型是:i
变量obj_a是否是const类型(取决于obj_a被创建时指定的类型):0
----------const类型返的函数----------
[修改前]int变量a的值是:100
不能修改,语法错误
[修改前]string变量b的值是:bbbb
不能修改,语法错误
[修改前]mutable int变量i的值是:24
[修改后]mutable int变量i的值是:1010
[修改前]mutable string变量j的值是:aaa
[修改后]mutable string变量j的值是:bbb

类之间的常见关系

使用已有类编写新的类有两种方式:两种基本关系:

继承关系和组合关系 (组合关系也就是包含关系)

  • 继承关系也称为“is a”关系或“是”关系
  • 组合关系也称为“has a”关系或“有”关系,表现为封闭类,即一个类以另一个类的对象作为成员变量。

互包含关系的类

在处理相对复杂的问题而需要考虑类的组合时,很可能遇到两个类相互引用的情况,这种情况称为循环依赖。举例如下:

class A {	// 类A的定义
	public:
		void f(B b);	// 以类B对象为形参的成员函数
};
class B {	// 类B的定义
	public:
		void g(A a);	// 以类A对象为形参的成员函数
}

类的大小

派生类:

派生类对象中包含基类成员变量,而且基类成员变量的存储位置位于派生类对象新增的成员变量之前。

派生类对象占用的存储空间大小,等于基类成员变量占用的存储空间大小加上派生类对象自身成员变量占用的存储空间大小对象占用的存储空间包含对象中各成员变量占用的存储空间。

出于计算机内部处理效率的考虑,为变量分配内存时,会根据其对应的数据类型,在存储空间内对变量的起始地址进行边界对齐。

可以使用sizeof( )函数计算对象占用的字节数对象的大小与普通成员变量有关,与成员函数和类中的静态成员变量无关,即普通成员函数、静态成员函数、静态成员变量、静态常量成员变量等均对类对象的大小没有影响。

对象

默认类成员访问说明符

在 C++ 中,默认的类成员变量访问类型取决于使用的访问说明符。如果没有显式地指定访问说明符,那么默认情况下:

  • 类中声明的数据成员默认为私有(private)访问。
  • 类中声明的成员函数默认为公有(public)访问。

这意味着在类定义中未指定访问说明符的成员都将具有上述默认访问权限。例如:

class MyClass {
    int x;  // 默认为 private
    void foo();  // 默认为 public
};

在上面的示例中,int x 声明为私有变量,只能在类的内部被访问。void foo() 声明为公有函数,可以从类的外部访问。 需要注意的是,尽管默认的访问权限是私有和公有,但仍然建议在类定义中显式指定访问说明符,以提高代码的可读性和可维护性。

访问对象成员的几种方法

要访问对象成员,可以使用以下几种方法:

  1. 直接成员访问:使用成员操作符(.)直接访问对象的成员。这适用于访问公共成员(包括公共变量和公共函数)。例如:
Class obj;
obj.memberVariable;     // 访问对象的成员变量
obj.memberFunction();   // 调用对象的成员函数
  1. 指针成员访问:使用指向对象的指针,通过成员操作符(->)来访问对象的成员。这适用于访问动态分配的对象或对象指针。例如:
Class* ptr = new Class();
ptr->memberVariable;       // 访问对象的成员变量
ptr->memberFunction();     // 调用对象的成员函数
delete ptr;                // 释放动态分配的对象内存
  1. 引用成员访问:使用对象的引用来访问对象的成员,可以像直接成员访问一样使用成员操作符(.)。这适用于引用已存在的对象。例如:
Class& ref = existingObj;
ref.memberVariable;        // 访问对象的成员变量
ref.memberFunction();      // 调用对象的成员函数
  1. this 指针:在对象成员函数中,可以使用 this 指针来访问对象自身的成员。this 是指向当前对象的指针,可以在成员函数中隐式地使用它来访问对象的成员变量和成员函数。例如:
void Class::memberFunction() {
    this->memberVariable;     // 使用 this 指针访问对象的成员变量
    this->anotherFunction();  // 使用 this 指针调用对象的另一个成员函数
}

这些方法适用于不同的场景和需求,根据具体情况选择合适的方法来访问对象的成员。请注意,私有成员只能通过公共接口或友元函数来访问,以保持封装性和数据的

成员对象和封闭类Enclosing

成员对象

成员对象是一个类中的另一个类的对象。

成员对象汇总示例1:Enclosing class的用法:

#include <iostream>
#include <string>
using namespace std;

//成员类;像subclass这种被其他类作为其变量类型或者函数返回值类型的类叫做成员类
class subclass{
private:
    int s_a;
    const int s_b = 10;
    static int s_c;

public:
    int pub_s_a = 100;
    const int pub_s_b = 200;
    static int pub_s_c;
    void showdata_1(){
        cout << "----------我是subclass的showdata_1()----------" << endl;
        cout << "变量s_a的值:" << s_a << endl;
        cout << "变量s_b的值:" << s_b << endl;
        cout << "变量s_c的值:" << s_c << endl;
        cout << "变量pub_s_a的值:" << pub_s_a << endl;
        cout << "变量pub_s_b的值:" << pub_s_b << endl;
        cout << "变量pub_s_c的值:" << pub_s_c << endl;
    }

    static void showdata_2(){
        cout << "----------我是subclass的showdata_2()----------" << endl;
        //cout << "变量s_a的值:" << s_a << endl;
        //cout << "变量s_b的值:" << s_b << endl;
        cout << "变量s_c的值:" << s_c << endl;
        //cout << "变量pub_s_a的值:" << pub_s_a << endl;
        //cout << "变量pub_s_b的值:" << pub_s_b << endl;
        cout << "变量pub_s_c的值:" << pub_s_c << endl;
    }
};

//主类;像mainclass这种成员变量的类型或者成员函数的返回值是subclass类;那么mainclass类就是主类
class mainclass{
private:
    int m_a;
    const int m_b =100;
    static int m_c;
    subclass test;

public:
    void test_use_subclass(){
        cout << "----------我是mainclass的test_use_subclass()----------" << endl;
        cout << "#尝试访问对象成员subclass的private变量#(语法错误)" << endl;
        cout << "#尝试访问对象成员subclass的public变量#" << endl;
        cout << "变量pub_s_a的值:" << test.pub_s_a << endl;
        cout << "变量pub_s_b的值:" << test.pub_s_b << endl;
        cout << "变量pub_s_c的值:" << test.pub_s_c << endl;
        cout << "变量pub_s_c的值:" << subclass::pub_s_c << endl;
        cout << "#尝试访问对象成员subclass的函数#(普通public函数)" << endl;
        test.showdata_1();
        cout << "#尝试访问对象成员subclass的函数#(static函数_实例访问)" << endl;
        test.showdata_2();
        cout << "#尝试访问对象成员subclass的函数#(static函数_静态访问)" << endl;
        subclass::showdata_2();
    }
};

int subclass::s_c;            //必须;被用到的静态变量都需要进行申明。
int subclass::pub_s_c;        //必须;被用到的静态变量都需要进行申明。
//mainclass::m_c为什么不用申明?因为在整个变量创建后整个程序中没有再用到这个变量。

int main(){
    cout << "----------1.通过mainclass执行----------" << endl;
    mainclass mainobj;
    mainobj.test_use_subclass();
    cout << "\n \n" << endl;

    subclass subobj;
    cout << "----------2.直接通过subclass执行----------" << endl;
    subclass::showdata_2();
    cout << "#尝试访问对象成员的public变量#" << endl;
    cout << "变量pub_s_a的值:" << subobj.pub_s_a << endl;
    cout << "变量pub_s_b的值:" << subobj.pub_s_b << endl;
    cout << "变量pub_s_c的值:" << subobj.pub_s_c << endl;
    cout << "变量pub_s_c的值:" << subclass::pub_s_c << endl;
    cout << "#尝试访问对象成员的函数#(普通public函数)" << endl;
    subobj.showdata_1();
    cout << "#尝试访问对象成员的函数#(static函数)" << endl;
    subobj.showdata_2();
    cout << "#尝试访问对象成员的函数#(static函数)" << endl;
    subclass::showdata_2();

    return 0;
}

输出结果:

----------1.通过mainclass执行----------
----------我是mainclass的test_use_subclass()----------
#尝试访问对象成员subclass的private变量#(语法错误)
#尝试访问对象成员subclass的public变量#
变量pub_s_a的值:100
变量pub_s_b的值:200
变量pub_s_c的值:0
变量pub_s_c的值:0
#尝试访问对象成员subclass的函数#(普通public函数)
----------我是subclass的showdata_1()----------
变量s_a的值:42
变量s_b的值:10
变量s_c的值:0
变量pub_s_a的值:100
变量pub_s_b的值:200
变量pub_s_c的值:0
#尝试访问对象成员subclass的函数#(static函数_实例访问)
----------我是subclass的showdata_2()----------
变量s_c的值:0
变量pub_s_c的值:0
#尝试访问对象成员subclass的函数#(static函数_静态访问)
----------我是subclass的showdata_2()----------
变量s_c的值:0
变量pub_s_c的值:0



----------2.直接通过subclass执行----------
----------我是subclass的showdata_2()----------
变量s_c的值:0
变量pub_s_c的值:0
#尝试访问对象成员的public变量#
变量pub_s_a的值:100
变量pub_s_b的值:200
变量pub_s_c的值:0
变量pub_s_c的值:0
#尝试访问对象成员的函数#(普通public函数)
----------我是subclass的showdata_1()----------
变量s_a的值:8
变量s_b的值:10
变量s_c的值:0
变量pub_s_a的值:100
变量pub_s_b的值:200
变量pub_s_c的值:0
#尝试访问对象成员的函数#(static函数)
----------我是subclass的showdata_2()----------
变量s_c的值:0
变量pub_s_c的值:0
#尝试访问对象成员的函数#(static函数)
----------我是subclass的showdata_2()----------
变量s_c的值:0
变量pub_s_c的值:0

封闭类

类的嵌套。

封闭类(Enclosing class)是指在面向对象编程中,一个类包含另一个类的情况。被包含的类称为内部类或嵌套类(Inner class or Nested class),而包含它的类称为外部类(Outer class)或封闭类。

内部类可以直接访问外部类的成员(包括私有成员),并且可以使用外部类的实例来创建内部类的对象。通过这种方式,封闭类提供了一种实现封装、组织和逻辑关联的方法。

封闭类汇总示例1:

#include <iostream>

using namespace std;

class outerclass{
private:
    int o_a = 10;
    //innerclass o_obj;     //语法错误,系统认为未定义;需要定义在内层类的定义之后。
    //innerclass& o_obj;     //不允许,系统认为未定义;需要定义在内层类的定义之后。

public:
    int o_b = 20;

    void o_test01(){
        cout << "----------我是outerclass的o_test01()----------" << endl;
    }

    void o_test02(){
        cout << "----------我是outerclass的o_test02()----------" << endl;
        //innerclass in_obj;   //不允许
        //innerclass& in_obj;   //引用变量需要初始化
        
        innerclass innerObj(*this); // 创建内层类对象  ;从外层类的访问内层类的方法1:实例+this指针
        cout << "[from outer]访问内层类的函数i_test01():"<< endl;
        innerObj.i_test01();     // 调用内层类的方法
        //cout << "访问内层类的变量: " << innerObj.i_a << endl;      //不能直接访问私有变量;这对于内层类来所;外层类属于类外。
        cout << "[from outer]访问内层类的变量: " << innerObj.i_b << endl;


    }


    class innerclass{
    private:
        int i_a = 100;
        //outerclass i_obj;        //语法错误,不允许这样做。在 innerclass 中试图声明一个 outerclass 对象的成员变量是不允许的,因为内部类对象的创建必须依赖于外部类对象的存在。
        outerclass& ref_obj;       //使用引用的方式为外层对象取别名
        
    public:
        int i_b = 200;
        int i_c = 300;
        int i_d = i_a + i_b + i_c;
        //构造函数;配合上面的outerclass& ref_obj;使用
        innerclass(outerclass& obj) : ref_obj(obj) {//在构造函数中初始化引用;若没有此句,在 innerclass 构造函数中就没有初始化这个引用,导致无法使用它访问 outerclass 成员。
        
        }

        //cout << "123" << endl;      //错误写法:在 innerclass 内部的构造函数之外,直接在类体中编写代码是非法的。
        //cout << "在innerclass中尝试访问outerclass的变量"<< ref_obj.o_a  << endl; //错误写法:在 innerclass 内部的构造函数之外,直接在类体中编写代码是非法的。

        void i_test01(){
            cout << "----------我是innerclass的i_test01()----------" << endl;
            cout << "----------访问outerclass资源from inner----------" << endl;
            cout << "[from inner]private变量o_a:" << ref_obj.o_a << endl;
            cout << "[from inner]public变量o_b:" << ref_obj.o_b << endl;
            cout << "[from inner]调用outerclass函数:o_test01()" << endl;
            ref_obj.o_test01();
        }

    };

    /*
    innerclass o_obj;       //从外层类的访问内层类的方法2:实例
    void o_test03(){
        cout << "----------我是outerclass的o_test03()----------" << endl;
        cout << "[from outer]访问内层类的变量: " << o_obj.i_b << endl;
    }
    */

   /*
   innerclass& o_obj;      //从外层类的访问内层类的方法3:引用
   void o_test03(){
        cout << "----------我是outerclass的o_test03()----------" << endl;
        cout << "[from outer]访问内层类的变量: " << o_obj.i_b << endl;
    }
    */

    
};

int main(){
    outerclass main_obj;
    main_obj.o_test02();

    return 0;
}

输出结果:

----------我是outerclass的o_test02()----------
[from outer]访问内层类的函数i_test01():
----------我是innerclass的i_test01()----------
----------访问outerclass资源from inner----------
[from inner]private变量o_a:10
[from inner]public变量o_b:20
[from inner]调用outerclass函数:o_test01()
----------我是outerclass的o_test01()----------
[from outer]访问内层类的变量: 200

封闭类中在外层类的内体外定义内层类

在C++中,内层类的定义通常是在外层类的类体内部进行的。然而,也可以将内层类的定义放在外层类的类体之外,但需要注意一些问题。

如果要在外层类的类体之外定义内层类,需要按以下步骤操作:

  1. 在外层类的声明后面加上分号。
  2. 在外层类声明结束后,定义内层类,包括成员变量和成员函数等。
  3. 外层类的方法或作用域中使用完整的内层类名称(包括外层类名称和内层类名称)。

例如,下面是一个示例代码:

#include <iostream>

class OuterClass {
public:
    void OuterMethod();

    class InnerClass;  // ***内层类的声明***

    int o_a = 10;
};

class OuterClass::InnerClass {   //在外层类的类体之外定义内层类
public:
    void InnerMethod() {
        std::cout << "调用内层类的方法" << std::endl;
    }

    int i_a = 100;
};

void OuterClass::OuterMethod() {
    std::cout << "调用外层类的方法" << std::endl;

    InnerClass innerObj;
    innerObj.InnerMethod();

    std::cout << "访问内层类的变量: " << innerObj.i_a << std::endl;
}

int main() {
    OuterClass obj;
    obj.OuterMethod();

    return 0;
}

在这个示例中,OuterClass 是外层类,在其类体之外定义了内层类 InnerClass。在外层类的方法 OuterMethod 中,可以直接使用内层类的方法和成员变量。

无论是将内层类定义在外层类的类体内部还是外部,都可以实现从外层类访问内层类的方法和成员变量。选择哪种方式取决于代码的组织和逻辑结构。

this指针

在C++中,this是一个特殊的指针,它指向当前对象的地址。在类的成员函数中,可以使用 this 指针来引用调用该函数的对象。

使用 this 的主要目的是区分成员变量和局部变量之间的命名冲突。当成员变量和局部变量同名时,使用 this-> 可以明确地指示使用的是成员变量。此外,this 还可以在类的成员函数内部返回对当前对象的引用,从而实现链式调用。

this指针只能用于类的成员函数中,它指向调用该成员函数的对象的地址。

this指针的使用示例

#include <iostream>

using namespace std;

class A{
private:
    int a = 10;

public:
    class B;
    
    void test1(){     //为啥要放在类体外定义呢?
        int a =100;
        cout << "----------我是outer class的test1()----------" << endl;
        cout << "print 'local var:a' is:" << a << endl;
        cout << "print 'global var:a' is:" << this->a << endl;
        cout << "----------我是outer class的test1(),现在调用outer class的test2()----------" << endl;
        test2();
        cout << "----------调用完成,回到outer class的test1()----------" << endl;
    }

    void test2(){
        int a =200;
        cout << "----------我是test2()----------" << endl;
        cout << "print 'local var:a' is:" << a << endl;
        cout << "print 'global var:a' is:" << this->a << endl;
        cout << "----------退出test2()----------" << endl;

    }

    void test3();

};


class A::B{
private:
    int a = 222;

public:
    void showvar(){
        int a = 333;
        cout << "----------我是inner class B的showvar()----------" << endl;
        cout << "print 'local var:a' is:" << a << endl;
        cout << "print 'global var:a' is:" << this->a << endl;
        cout << "----------现在调用outer class B的test2()----------[无法调用]" << endl;
        cout << "----------现在调用inner class B的showvar2()----------" << endl;
        this->showvar2();
        cout << "----------调用完成,回到inner class B的showvar()----------" << endl;
    }

    void showvar2(){
        int a =444;
        cout << "----------我是inner class B的showvar2()----------" << endl;
        cout << "print 'local var:a' is:" << a << endl;
        cout << "print 'global var:a' is:" << this->a << endl;
        cout << "----------退出inner class B的showvar2()----------" << endl;

    }


};


//在类体外;A::B::showvar()定义之后进行定义
void A::test3(){     //为啥要放在类体外定义呢?
    int a =300;
    cout << "----------我是outer class的test3(),现在调用inner class B的showvar()----------" << endl;
    B testobj;
    testobj.showvar();      //因为如果在类体中定义,那么此处调用B的showvar()因为在B中尚未完成定义(仅做了申明),会导致系统找不到这个函数。
    //因此,需要将调用尚未完成定义的函数的函数放在被调用函数之后。所以上面的问题其实不是为什么要放在类体外定义的问题,而是需要将调用函数放在被调用函数定义之后的问题。


}


int main(){
    A obj;
    obj.test1();
    cout << "\n\n" << endl;

    obj.test3();

    return 0;
}

输出结果:

----------我是outer class的test1()----------
print 'local var:a' is:100
print 'global var:a' is:10
----------我是outer class的test1(),现在调用outer class的test2()----------
----------我是test2()----------
print 'local var:a' is:200
print 'global var:a' is:10
----------退出test2()----------
----------调用完成,回到outer class的test1()----------



----------我是outer class的test3(),现在调用inner class B的showvar()----------
----------我是inner class B的showvar()----------
print 'local var:a' is:333
print 'global var:a' is:222
----------现在调用outer class B的test2()----------[无法调用]
----------现在调用inner class B的showvar2()----------
----------我是inner class B的showvar2()----------
print 'local var:a' is:444
print 'global var:a' is:222
----------退出inner class B的showvar2()----------
----------调用完成,回到inner class B的showvar()----------

TIPs:

在C++中,成员函数通过一个隐含的指针this来访问其所属对象的成员变量和其他成员函数。this指针指向当前调用该成员函数的对象。

当我们使用对象调用类的成员函数时,编译器会自动将对象的地址作为this指针传递给成员函数。这样,在成员函数内部,我们就可以使用this指针来访问该对象的成员。

静态对象与this指针

静态成员变量在类的所有对象之间共享,它们与特定的对象实例无关。因此,对于静态成员变量,不能使用this指针来访问。

在C++中,对于静态成员变量的访问,应该使用类名和作用域解析操作符::进行访问,而不是通过this指针。

示例:

#include <iostream>

using namespace std;

class A{
private:
    int a = 10;
    static int b;

public:
    A(){
        A::b = 20; 
    };

    void test1(){
        cout << "直接访问变量a:" << a << endl;
        cout << "直接访问变量b:" << b << endl;
        cout << "通过this指针访问变量this->a:" << this->a << endl;
        cout << "(不建议)通过this指针访问变量this->b:" << this->b << endl;
        cout << "通过::访问变量A::a:" << A::a << endl;
        cout << "通过::访问变量A::b:" << A::b << endl;
    }


    void static test2(){
        //cout << "直接访问变量a:" << a << endl;     //静态函数只能访问静态的变量或函数
        cout << "直接访问变量b:" << b << endl;
        //cout << "通过this指针访问变量this->a:" << this->a << endl;   //静态函数只能访问静态的变量或函数;且this不能在静态函数中使用
        //cout << "(不建议)通过this指针访问变量this->b:" << this->b << endl;   //静态函数只能访问静态的变量或函数;且this不能在静态函数中使用
        //cout << "通过::访问变量A::a:" << A::a << endl;   //静态函数只能访问静态的变量或函数
        cout << "通过::访问变量A::b:" << A::b << endl;
    }

};

int A::b;


int main(){
    A obj;
    obj.test1();

    cout << "\n \n" << endl;

    obj.test2();

    return 0;
}

输出结果:

直接访问变量a:10
直接访问变量b:20
通过this指针访问变量this->a:10
(不建议)通过this指针访问变量this->b:20
通过::访问变量A::a:10
通过::访问变量A::b:20



直接访问变量b:20
通过::访问变量A::b:20

派生类如何利用this指针访问基类成员

在C++中,使用this指针来访问父类的成员变量或函数是不直接支持的,因为this指针只指向当前对象的成员变量和函数。但是,可以通过在派生类中使用作用域解析运算符::来访问父类的成员。 下面是使用this指针通过作用域解析运算符来访问父类成员变量和函数的示例:

#include <iostream>
using namespace std;

class Parent {
public:
    int parentVar;

    void parentFunc() {
        cout << "Parent Function" << endl;
    }

};

class Child : public Parent {
public:
    void childFunc() {
        this->parentVar = 10;  // 访问父类成员变量
        this->parentFunc();   // 访问父类成员函数
    }
};

int main() {
    Child childObj;
    childObj.childFunc();

    return 0;

}

在示例中,Child类继承自Parent类。在childFunc()函数中,使用this指针来访问父类的成员变量parentVar和父类的成员函数parentFunc()。通过this指针和作用域解析运算符::,可以在派生类中访问父类的成员。 注意,在派生类中,可以直接访问父类的公有成员。如果父类的成员变量或函数是私有的,派生类无法直接访问,需要使用父类提供的公有接口进行访问。

继承与派生

基础概念

继承与派生是人类认识世界的过程。

继承与派生是面向对象编程中的两个重要概念。

继承(Inheritance)是指一个类(称为子类或派生类)从另一个类(称为父类或基类)获取属性和方法的过程。子类可以继承父类的非私有成员(包括成员变量和成员函数),这使得子类可以复用父类的代码并扩展功能。继承关系表达了类与类之间的"is-a"关系,即子类是父类的一种特殊情况。

派生(Derivation)是指通过继承创建派生类的过程。派生类可以通过继承获得父类的属性和方法,并且可以添加新的成员变量和成员函数,以实现额外的功能或修改继承的行为。派生类还可以对继承自父类的成员进行重写(覆盖),即在派生类中重新定义具有相同名称和参数列表的成员函数来改变其行为。

通过继承和派生,可以构建类的层次结构(Class Hierarchy)。在这个层次结构中,顶层是根类(或基类),它是其他类的父类,而其他类则逐级派生自根类。这种层次结构可以使代码更加模块化、可维护和可扩展。

除了继承功能,派生类还可以访问父类的公有和受保护成员(私有成员对派生类不可见),并可以通过调用父类的构造函数来初始化继承的成员。

需要注意的是,在设计继承关系时,要遵循开闭原则(Open-Closed Principle)和单一职责原则(Single Responsibility Principle),确保继承的正确使用,使代码更加清晰、灵活和可复用。

总结来说,继承与派生是面向对象编程中用于代码复用和扩展的重要概念。通过继承,子类可以从父类获取属性和方法;通过派生,子类可以在继承的基础上添加、修改和覆盖成员,以满足特定的需求。

示例

#include <iostream>

using namespace std;

//基类
class base{
public:
    base(){
        base_d = 40;
        base_f = 60;
    }

private:
    int base_a = 10;
    static const int base_b = 20;    //注意这列static和const一起用其实不是合理的用法;虽然本实例主张不会报错,但在某些情况下会导致链接错误的发生。

    void base_pri_test01(){
        cout << "我是父类base的private类型函数:base_pri_test01()" << endl;
    }

    void base_pri_write(){
        cout << "我会写作..." << endl;
    }

public:
    int base_c = 30;
    static int base_d;

    void base_pub_test01(){
        cout << "我是父类base的public类型函数:base_pub_test01()" << endl;
    }

    void base_pub_football(){
        cout << "我会踢足球..." << endl;
    }

    void reloadfun(){  //测试父子类函数重载
        cout << "我是父类base的public类型函数:reloadfun()" << endl;
    }


protected:
    int base_e = 50;
    static int base_f;

    void base_pro_test01(){
        cout << "我是父类base的protected类型函数:base_pro_test01()" << endl;
    }

    void base_pro_coding(){
        cout << "我会编程..." << endl;
    }

    void samefun(){  //测试父子类同名函数问题
        cout << "我是父类base的protected类型函数:samefun()" << endl;
    }

};


class subA:base{
private:
    int subA_a = 210;

public:
    void sub_pub_music(){
        cout << "子类subA爱好:音乐" << endl;
    }

    //可以在派生类最终使用基类的全部成员
    void test(){
        //cout << "[禁止]使用基类的private变量base_a:" << base_a << endl;  //父类的private成员不能直接被子类访问;需要通过父类的函数接口间接访问。
        cout << "使用基类的public变量base_c:" << base_c << endl;
        cout << "使用基类的protected变量base_e:" << base_e << endl;
        cout << "[禁止]使用基类的private函数:base_pri_write()" << endl;
        //base_pri_write();     //父类的private成员不能直接被子类访问;需要通过父类的函数接口间接访问。
        cout << "使用基类的public函数:base_pub_football()" << endl;
        base_pub_football();
        cout << "使用基类的protected函数:base_pro_coding()" << endl;
        base_pro_coding();
    }

    void samefun(){   //测试父子类同名函数问题
        cout << "我是子类subA的Public类型函数:samefun()" << endl;
    }

};


class subB:public base{
private:
    int subA_a = 310;

public:
    void sub_pub_moive(){
        cout << "子类subB爱好:电影" << endl;
    }

    void reloadfun(int x){  //测试父子类函数重载
        cout << "我是父类base的protected类型函数:reloadfun()" << endl;
    }
};


class subC:private base{
private:
    int subA_a = 410;

public:
    void sub_pub_food(){
        cout << "子类subA爱好:美食" << endl;
    }
};

class subD:protected base{
private:
    int subA_a = 510;

public:
    void sub_pub_game(){
        cout << "子类subA爱好:游戏" << endl;
    }
};


int base::base_d;
int base::base_f;



int main(){
    base baseobj;
    subA subobj_a;
    subB subobj_b;
    subC subobj_c;
    subD subobj_d;

    cout << "----------父类base----------" << endl;
    //baseobj.base_pri_write();         //语法错误:类的私有成员在类外不允许访问。
    baseobj.base_pub_test01();
    //baseobj.base_pro_coding();        //语法错误:类的受保护成员在类外不允许访问。

    cout << "----------子类subA(default:private)----------" << endl;
    //subobj_a.base_pub_test01();         //禁止访问:默认的private关键字将从父类继承过来的public和protected成员变成自己的private成员。
    //subobj_a.base_pro_coding();         //禁止访问:默认的private关键字将从父类继承过来的public和protected成员变成自己的private成员。
    subobj_a.sub_pub_music();
    cout << "测试子类中访问父类不同访问修饰符的成员" << endl;
    subobj_a.test();
    cout << "测试父子类中同名函数访问" << endl;
    subobj_a.samefun();
    


    cout << "----------子类subB(public)----------" << endl;
    subobj_b.base_pub_football();
    //subobj_b.base_pro_coding();          //禁止访问:public关键字将从父类继承过来的public变成自己的Public成员;父类的protected和private成员仍旧是protected和private成员。
    subobj_b.sub_pub_moive();
    cout << "测试父子类函数的重载(继承中被子类覆盖)" << endl;
    /* 需要使用到虚函数实现多态;若不使用虚函数则父子函数同名但参数不太不能构成重载;他们之间互相独立(也就是分片) */
    // subobj_b.reloadfun(); //错误写法,实际上只会调用subB子类的reloadfun()函数;胡ibei检查到参数不符合要求
    subobj_b.reloadfun(1);
    baseobj.reloadfun();

    cout << "----------子类subC(private)----------" << endl;
    //subobj_c.base_pub_football();            //禁止访问:默认的private关键字将从父类继承过来的public和protected成员变成自己的private成员。
    //subobj_c.base_pro_coding();          //禁止访问:默认的private关键字将从父类继承过来的public和protected成员变成自己的private成员。
    subobj_c.sub_pub_food();

    cout << "----------子类subD(protected)----------" << endl;
    //subobj_d.base_pub_football();             //禁止访问:protected关键字将从父类继承过来的public成员变成自己的protectede成员;父类的protected和private成员仍旧是protected和private成员。
    //subobj_d.base_pro_coding();             //禁止访问:protected关键字将从父类继承过来的public成员变成自己的protectede成员;父类的protected和private成员仍旧是protected和private成员。
    subobj_d.sub_pub_game();
    

    return 0;
}

输出结果:

----------父类base----------
我是父类base的public类型函数:base_pub_test01()
----------子类subA(default:private)----------
子类subA爱好:音乐
测试子类中访问父类不同访问修饰符的成员
使用基类的public变量base_c:30
使用基类的protected变量base_e:50
[禁止]使用基类的private函数:base_pri_write()
使用基类的public函数:base_pub_football()
我会踢足球...
使用基类的protected函数:base_pro_coding()
我会编程...
测试父子类中同名函数访问
我是子类subA的Public类型函数:samefun()
----------子类subB(public)----------
我会踢足球...
子类subB爱好:电影
测试父子类函数的重载(继承中被子类覆盖)
我是父类base的protected类型函数:reloadfun()
我是父类base的public类型函数:reloadfun()
----------子类subC(private)----------
子类subA爱好:美食
----------子类subD(protected)----------
子类subA爱好:游戏

在定义派生类时,可以使用以下关键字来修饰父类:

  1. public:公有继承。以public关键字修饰的派生类会使得父类中的公有成员在派生类中仍然是公有的,保持访问权限不变。受保护成员和私有成员在派生类中仍然是受保护和私有的,对外部不可见。
  2. protected:保护继承。以protected关键字修饰的派生类会使得父类中的公有成员变为派生类的受保护成员,在派生类内部和派生类的子类中可访问,但对外部是不可见的。受保护成员和私有成员在派生类中仍然是受保护和私有的,对外部不可见。
  3. private:私有继承。以private关键字修饰的派生类会使得父类中的公有和受保护成员都变为派生类的私有成员,在派生类内部可访问,对外部和派生类的子类是不可见的。

需要注意的是,修饰父类的关键字只影响从父类继承来的成员在派生类中的访问权限,而不影响派生类自身定义的成员。

基类与派生类的友元函数

如果基类有友元类或友元函数,则其派生类不会因继承关系而也有此友元类或友元函数。

如果基类是某类的友元,则这种友元关系是被继承的。

即被派生类继承过来的成员函数,如果原来是某类的友元函数,那么它作为派生类的成员函数仍然是某类的友元函数。

总之,基类的友元不一定是派生类的友元;基类的成员函数是某类的友元函数,则其作为派生类继承的成员函数仍是某类的友元函数。

基类与派生类的静态成员

派生类会继承基类的静态成员。当派生类从基类派生时,它继承了基类的所有成员,包括静态成员。派生类可以直接访问和使用继承的静态成员。

需要注意的是,派生类可以通过继承的静态成员来访问基类的静态成员,但它们是独立的,互不影响。派生类也可以在其内部重新定义具有相同名称的静态成员,此时该成员将隐藏基类的静态成员。如果需要访问基类的隐藏静态成员,可以使用基类名加作用域运算符来访问。

基类与派生类中成员重名问题

派生类和基类中都可以定义自己的成员变量和成员函数,派生类中的成员函数可以访问基类中的公有成员变量,但不能直接访问基类中的私有成员变量。也就是说不能在派生类的函数中,使用

  • “基类对象名.基类私有成员函数(实参)”,
  • 或是“基类对象名.基类私有成员变量”,
  • 或是“基类名:基类私有成员”

的形式访问基类中的私有成员。 在类的派生层次结构中,基类的成员和派生类新增的成员都具有类作用域。二者的作用范围不同,是相互包含的两个层,派生类在内层,基类在外层。如果派生类声明了一个和基类某个成员同名的新成员,派生的新成员就隐藏了外层同名成员,直接使用成员名只能访问到派生类的成员。如果派生类中声明了与基类成员函数同名的新函数,即使函数的参数表不同,从基类继承的同名函数的所有重载形式也都会被隐藏如果要访问被隐藏的成员,就需要使用基类名和作用域分辨符来限定

多重继承

C++允许从多个类派生一个类,即一个派生类可以同时有多个基类。这称为多重继承。相应地,从一个基类派生一个派生类的情况,称为单继承或单重继承。一个类从多个基类派生的一般格式如下

class派生类名: 继承方式说明符 基类名1, 继承方式说明符 基类名2..., 继承方式说明基类名n
{
	类体

}
  • 派生类继承了基类名1、基类名2、、基类名n的所有成员变量和成员函......数,各基类名前面的继承方式说明符用于限制派生类中的成员对该基类名中成员的访问权限,其规则与单继承情况一样。
  • 多重继承情况下如果多个基类间成员重名时,按如下方式进行处理:
  • 对派生类而言,不加类名限定时默认访问的是派生类的成员;
  • 而要访问基类重名成员时,要通过类名加以限定。

多层次的派生

在C++中,派生可以是多层次的。例如,类CStudent派生类CGraduatedStudent,而后者又可以派生CDoctorStudent等。总之,类A派生类B,类B可以再派生类C,类C又能够派生类D,以此类推。

在这种情况下,称类A是类B的直接基类,类B是类C的直接基类类A是类C的间接基类。当然,类A也是类D的间接基类。在定义派生类时,只需写直接基类,不需写间接基类。派生类沿着类的层次自动向上继承它所有的直接和间接基类的成员在C++中,类之间的继承关系具有传递性。

派生类的成员包括派生类自己定义的成员、直接基类中定义的成员及所有间接基类中定义的全部成员。

当生成派生类的对象时,会从最顶层的基类开始逐层往下执行所有基类的构造函数,最后执行派生类自身的构造函数;当派生类对象消亡时,会先执行自身的析构函数,然后自底向上依次执行各个基类的析构函数。

继承与访问关系

public继承

private继承

protected继承

保护继承中,基类的公有成员和保护成员都以保护成员的身份出现在派生类中,而基类的私有成员不可以直接访问。这样,派生类的其他成员可以直接访问从基类继承来的公有和保护成员,但在类外通过派生类的对象无法直接访问它们。

基类与派生类的构造与析构函数

派生类并不继承基类的构造函数,所以需要在派生类的构造函数中调用基类的构造函数,以完成对从基类继承的成员变量的初始化工作。具体来说,派生类对象在创建时,除了要调用自身的构造函数进行初始化外,还要调用基类的构造函数初始化其包含的基类成员变量 在执行一个派生类的构造函数之前,总是先执行基类的构造函数。派生类对象消亡时,先执行派生类的析构函数,再执行基类的析构函数。

定义派生类构造函数的一般格式如下:

派生类名::派生类名(参数表): 基类名1(基类1 初始化参数表),...,基类名m(基类m初始化参数表),成员对象名1(成员对象1 初始化参数表)..,成员对象名n(成员对象n 初始化参数表)
{
	类构造函数函数体         //其他初始化操作
}

派生类构造函数执行的一般次序如下1)调用基类构造函数,调用顺序按照它们被继承时声明的顺序 (从左向右2)对派生类新增的成员变量初始化,i调用顺序按照它们在类中声明的顺序3)执行派生类的构造函数体中的内容 构造函数初始化列表中基类名、对象名之间的次序无关紧要,它们各自出现的顺序可以是任意的,无论它们的顺序怎样安排,基类构造函数的调用和各个成员变量的初始化顺序都是确定的。

基类与派生类指针的互相转换

在公有派生的情况下,因为派生类对象也是基类对象,所以派生类对象可以赋给基类对象。

对于指针类型,可以使用基类指针指向派生类对象,也可以将派生类的指针直接赋值给基类指针。

但即使基类指针指向的是一个派生类的对象,也不能通过基类指针访问基类中没有而仅在派生类中定义的成员函数。

示例1

#include<iostream>

using namespace std;

class CBase {
protected:
    int n;
public:
    CBase(int i): n(i) {}
    void print() {
        cout << "CBase:n = " << n << endl;
    }
};
class CDerived: public CBase {
public:
    int v;
    CDerived(int i): CBase(i), v(2 * i) {}
    void Func() {};
    void print() {
        cout << "CDerived:n=" << n << endl;
        cout << "CDerived:v=" << v << endl;
    }
};


int main() {
    CDerived objDerived(3);
    CBase objBase(5);
    CBase * pBase = &objDerived;    // 使用基类指针指向派生类对象
    CDerived *pDerived;
    pDerived = &objDerived;
    cout << "使用派生类指针调用函数" << endl;
    pDerived -> print();    // 调用的是派生类中的函数
    pBase = pDerived;   // 基类指针 = 派生类指针,正确
    cout << "使用基类指针调用函数" << endl;
    pBase -> print();   // 调用的是基类中的函数
    //pBase -> Func()   // 错误,通过基类指针不能调用派生类函数
    //pDerived = pBase; // 错误,派生类指针=基类指针
    pDerived = (CDerived*)pBase;    // 强制类型转换,派生类指针=基类指针
    cout << "使用派生类指针调用函数" << endl;
    pDerived -> print();    // 调用的是派生类中的函数
    return 0;
}

示例2

#include <iostream>

using namespace std;

class base{
public:
    int a = 10;
    int b = 20;
    void test1(){
        cout << "I'm base class func: test1()." << endl;
    }

    void test2(){
        cout << "I'm base class func: test2()." << endl;
    }

};


class sub: public base{
public:
    int c = 30;
    int d = 40;
    void test1(){
        cout << "I'm sub class func: test1()." << endl;
    }

    void test3(){
        cout << "I'm sub class func: test3()." << endl;
    }

};



int main(){
    base baseobj;    //创建父类base对象
    sub subobj;      //创建子类sub的对象
    cout << "baseobj对象的地址:" << &baseobj << endl;
    cout << "subobj对象的地址:" << &subobj << endl;
    cout << "\n \n" << endl;


    base* pbase;     //创建base类指针变量;不建议这种写法,这种写法会导致未经初始化的指针变量的值是不确定的;建议写成:base* pbase = nullptr; 
    sub* psub;       //创建sub类指针变量
    cout << "指针变量的pbase的值:" << pbase << endl;
    //cout << "指针变量的pbase指向的值:" << *pbase << endl;     //非法语句;不能对未赋值的指针变量进行解引用
    cout << "指针变量的psub的值:" << psub << endl;
    //cout << "指针变量的psub指向的值:" << *psub << endl;     //非法语句;不能对未赋值的指针变量进行解引用
    cout << "\n \n" << endl;


    base* pbase1 = nullptr;     //创建base类指针变量,并设置为空指针。
    sub* psub1 = nullptr;       //创建sub类指针变量,并设置为空指针。
    cout << "指针变量的pbase1的值:" << pbase1 << endl;
    //cout << "指针变量的pbase1指向的值:" << *pbase1 << endl;     //非法语句;不能对空指针变量进行解引用
    cout << "指针变量的psub1的值:" << psub1 << endl;
    //cout << "指针变量的psub1指向的值:" << *psub1 << endl;     //非法语句;不能对空指针变量进行解引用
    if(pbase1 == nullptr){
        cout << "指针变量的pbase1是空指针" << endl;
    }else{
        cout << "指针变量的pbase1的值:" << pbase1 << endl;
    }
    cout << "\n \n" << endl;


    pbase1 = &baseobj;
    psub1 = &subobj;
    cout << "指针变量的pbase1的值(成员变量):" << pbase1->a << endl;
    cout << "指针变量的pbase1的值(成员函数):" << endl;
    pbase1->test1();
    pbase1->test2();
    cout << "指针变量的psub1的值(成员变量):" << psub1->a << endl;
    cout << "指针变量的psub1的值(成员变量):" << psub1->c << endl;
    cout << "指针变量的psub1的值(成员函数):" << endl;
    psub1->test1();
    psub1->test2();
    psub1->test3();
    cout << "\n \n" << endl;

    pbase1 = &subobj;        //基类的指针可以指向派生类对象
    //psub1 = &baseobj;        //派生类的指针不能指向基类对象
    cout << "指针变量的pbase1的值(成员变量):" << pbase1->a << endl;
    cout << "指针变量的pbase1的值(成员函数):" << endl;
    pbase1->test1();
    pbase1->test2();
    //pbase1->test3();         //即使base类型指针变量pbase指向了派生类的对象;但依旧不能使用base类中没有但是派生类中有的资源。
    //cout << "指针变量的pbase1的值(派生类独有成员变量):" << pbase1->c << endl;    //即使base类型指针变量pbase指向了派生类的对象;但依旧不能使用base类中没有但是派生类中有的资源。



    return 0;
}

输出结果:

baseobj对象的地址:0x61fdf8
subobj对象的地址:0x61fde0



指针变量的pbase的值:0x10
指针变量的psub的值:0xd81890



指针变量的pbase1的值:0
指针变量的psub1的值:0
指针变量的pbase1是空指针



指针变量的pbase1的值(成员变量):10
指针变量的pbase1的值(成员函数):
I'm base class func: test1().
I'm base class func: test2().
指针变量的psub1的值(成员变量):10
指针变量的psub1的值(成员变量):30
指针变量的psub1的值(成员函数):
I'm sub class func: test1().
I'm base class func: test2().
I'm sub class func: test3().



指针变量的pbase1的值(成员变量):10
指针变量的pbase1的值(成员函数):
I'm base class func: test1().
I'm base class func: test2().

函数与函数模板

函数模板

设计程序中的函数时,可能会遇到函数中参数的类型有差异,但需要实现的功能类似的情形。函数重载可以处理这种情形。重载函数的参数表中,可以写不同类型的参数,从而可以处理不同的情形。

为了提高效率,实现代码复用,C++提供了一种处理机制,即使用函数模板函数在设计时并不使用实际的类型,而是使用虚拟的类型参数。这样可以不必为每种不同的类型都编写代码段。当用实际的类型来实例化这种函数时将函数模板与某个具体数据类型连用。编译器将以函数模板为样板,生成一个函数,即产生了模板函数,这个过程称为函数模板实例化

函数模板实例化的过程由编译器完成。

程序设计时并不给出相应数据的类型,编译时,由编译器根据实际的类型进行实例化。

函数模板 VS. 函数

虽然函数模板的使用形式与函数类似,但二者有本质的区别,主要表现在以下3个方面:

  1. 函数模板本身在编译时不会生成任何目标代码,只有当通过模板生成具体的函数实例时才会生成目标代码。
  2. 被多个源文件引用的函数模板,应当连同函数体一同放在头文件中,而不能像普通函数那样只将声明放在头文件中。
  3. 函数指针也只能指向模板的实例,而不能指向模板本身。

函数或函数模板调用语句的匹配顺序(优先级)

函数与函数模板也是允许重载的。在函数和函数模板名字相同的情况下;一条函数调用语句到底应该被匹配成对哪个函数或哪个模板的调用呢?

C++编译器遵循以下先后顺序:

1)先找参数完全匹配的普通函数 (不是由模板实例化得到的模板函数)。

2)再找参数完全匹配的模板函数。

3)然后找实参经过自动类型转换后能够匹配的普通函数。

4)如果上面的都找不到,则报错。

示例

示例1:

#include <iostream>

using namespace std;

template <class X>
void myswap(X& a, X& b){
    X temp = a;
    a = b;
    b = temp;
}


int main(){
    int x = 10;
    int y = 100;
    cout << "交换前x=" << x << ";y=" << y << endl;
    myswap(x, y);           // 实例化函数模板swap,并交换x和y的值
    cout << "交换后x=" << x << ";y=" << y << endl;


    string ss = "hello";
    string tt = "xiamingliang";
    cout << "交换前ss=" << ss << ";tt=" << tt << endl;
    myswap(ss, tt);           // 实例化函数模板swap,并交换ss和tt的值
    cout << "交换后ss=" << ss << ";tt=" << tt << endl;


    return 0;
}

输出结果:

交换前x=10;y=100
交换后x=100;y=10
交换前ss=hello;tt=xiamingliang
交换后ss=xiamingliang;tt=hello

可以明显看到:函数模板使得代码更加灵活和可复用,能够适应不同类型的数据,并生成对应的函数代码。这样可以减少代码的重复编写,提高开发效率。

同一个函数模板多个typename的情况

#include <iostream>

template <class X1, typename X2, typename X3>       //可以使用关键字typename(推荐)或class来声明模板参数。它们在语义上是等价的,可以互换使用。
void myfunction(X1& a, X2& b, X3& c){
    std::cout << "a的值是:" << a << std::endl;
    std::cout << "b的值是:" << b << std::endl;
    std::cout << "c的值是:" << c << std::endl;
}

template <typename A, typename B>
void test01(A a, B b){
    std::cout << "您输入的a是:" << a << std::endl;
    std::cout << "您输入的b是:" << b << std::endl;
}

int main(){
    {   //用大括号将使用相同名字的变量分割为不通的作用域;防止出现编译错误。
        std::string x = "hello";
        int y = 100;
        float z = 1.224356;
        myfunction(x, y, z);
    }

    {
        int x = 10;
        int y = 500;
        std::string z = "xiamingliang";
        myfunction(x, y, z);
    }
    
    {
        int x = 999;
        double y = 3.22345678;
        test01<int, double>(x, y);
        test01<int>(x, y);    //可以省略后面的类型;显式指定模板参数类型为int。这样,函数模板会被实例化为处理int类型的参数,并输出相应的结果。
        //test01<,double>(x, y);       //不能省略前面的类型
        test01(x, y);       //但可以全部省略

    }
    

    return 0;
}

输出结果:

a的值是:hello
b的值是:100
c的值是:1.22436
a的值是:10
b的值是:500
c的值是:xiamingliang
您输入的a是:999
您输入的b是:3.22346
您输入的a是:999
您输入的b是:3.22346
您输入的a是:999
您输入的b是:3.22346

类模板与向量

类模板

通过类模板,可以实例化一个个的类。继承机制也是在一系列的类之间建立某种联系,这两种涉及多个类的机制是有很大差异的。类是相同类型事物的抽象,有继承关系的类可以具有不同的操作。而模板是不同类型的事物具有相同的操作,实例化后的类之间没有联系,相互独立。

一般格式:

template <typename T>
class 类模板名{
	类体... ...

};

其中,“模板参数表”的形式与函数模板中的“模板参数表”完全一样。类体定义与普通类的定义几乎相同,只是在它的成员变量和成员函数中通常要用到模板的类型参数。 类模板的成员函数既可以在类体内进行说明,也可以在类体外进行说明。如果在类体内定义,则自动成为内联函数。如果需要在类模板以外定义其成员函数,则要采用以下格式。

template <typename T>
class 类模板名{
	类体... ...
    
    返回值类型 成员函数名(参数表);          //外部定义的函数的申明

};
    
    
template <typename T>
返回值类型 类模板名<模板参数标识符列表>::成员函数名(参数表){
    函数体… …
}

类模板声明本身并不是一个类,它说明了类的一个家族。只有当被其他代码引用时,模板才根据引用的需要生成具体的类。

不能使用类模板来直接生成对象,因为类型参数是不确定的,必须先为模板参数指定“实参”,即模板要“实例化”后,才可以创建对象。也就是说,当使用类模板创建对象时,要随类模板名给出对应于类型形参或普通形参的具体实参,格式如下:

类模板名 <模板参数表> 对象名1, ..., 对象名n;

或者

类模板名 <模板参数表> 对象名1(构造函数实参), ..., 对象名n(构造函数实参);
  • 编译器由类模板生成类的过程称为类模板的实例化。
  • 由类模板实例化得到的类称为模板类。
  • 要注意的是,与类型形参相对应的实参是类型名.

示例

#include <iostream>
#include <iomanip> // 包含这个头文件用于设置输出精度和格式。

using namespace std;

template <typename T>
class test{
    public:
        T list[10];   //定义一个长度为10的数组,数组元素的类型是T。
        T testfunc(int i);     //申明一个名为testfunc的函数,该函数需要一个整型的参数i;该函数运行完成后将返回一个类型为T的返回值。

};



template <typename T>
T test<T>::testfunc(int i){
    if(i < 10){
        return list[i];
    }else{
        cout << "已超出list的最大范围10。" << endl;
        return list[0];
    }
}



int main(){
    //第一个实例
    test<char> obj;      //直接test obj;是错误的,必须test<xxx>为模板类指定变量类型。
    int k;
    char str[20] = "hello world!";

    for(k=0; k<10; k++){
        obj.list[k] = str[k];
    }

    cout << "赋值后obj.list的值为:" << obj.list << endl;       //这里不符合C++语法;意会即可。

    for(k=0; k<=5; k++){
        char res = obj.testfunc(k);
        cout << "取到第" << k << "个值是:" << res << endl;
    }

    cout << endl;
    cout << "----------" << endl;


    //第二个实例
    test<double> obj2;
    int j;
    double data[10] = {1.23, 2.34, 3.45, 4.56, 5.67, 6.78, 7.89, 0.99, 12.34};
    for(j=0; j<=5; j++){
        // cout << data[j] << endl;
        obj2.list[j] = data[j];
    }
    cout << "赋值后obj2.list的值为:" << obj2.list << endl;        //这里不符合C++语法;意会即可。
    for(j=0; j<=5; j++){
        double res = obj2.list[j];
        cout << "取到第" << j << "个值是:" << res << endl;
    }

    return 0;
}

输出结果:

赋值后obj.list的值为:hello worl@
取到第0个值是:h
取到第1个值是:e
取到第2个值是:l
取到第3个值是:l
取到第4个值是:o
取到第5个值是:

----------
赋值后obj2.list的值为:0x61fd80
取到第0个值是:1.23
取到第1个值是:2.34
取到第2个值是:3.45
取到第3个值是:4.56
取到第4个值是:5.67
取到第5个值是:6.78

意义:

减少了代码量。对不同类型的输入执行相同操作是将输入类型变量化形成模板;编译时根据指定的类型有模板生成实际的对象。

类模板的继承

类之间允许继承,类模板之间也允许继承。具体来说,类模板和类模板之间、类模板和类之间可以互相继承,它们之间的常见派生关系有以下4种情况:

  1. 普通类继承模板类
  2. 类模板继承普通类
  3. 类模板继承类模板
  4. 类模板继承模板类

根据类模板实例化的类即是模板类。

示例1:普通类继承类模板

#include <iostream>

using namespace std;

template<typename T>
class T_base{       //类模板 基类
public:
    T tb_a;

    void tbtest(){
        cout << "我是T_base类的成员函数:tbtest();输出变量tb_a的值为:" << tb_a << endl;
    };

};

class T_sub:public T_base<int>{};      // 从类模板继承的类 普通类;实质上对模板仅限了实例化。

int main(){
    T_sub obj;
    obj.tb_a = 20;
    obj.tbtest();

    /*
    obj.tb_a = "test";      // 会因类型不匹配到到组织编译失败。
    obj.tbtest();
    */

    return 0;
}

输出结果:

我是T_base类的成员函数:tbtest();输出变量tb_a的值为:20

示例2:类模板继承普通类

#include <iostream>

using namespace std;

class base{
public:
    int base_a = 10;
    void show(){
        cout << "我是base类的show();将显示变量base_a:" << base_a << endl;
    }

    void show1(){
        cout << "我是base类的show1();将显示变量base_a:" << base_a << endl;
    }
};


template <class T>     // 同temlate<typename T>
class T_sub:public base{
public:
    T sub_a;
    void show(){
        cout << "我是T_sub类的show();将显示变量sub_a:" << sub_a << endl;
    }

    void test(){
        cout << "我是T_sub类的test();将显示变量sub_a:" << sub_a << endl;
    }
};


int main(){
    T_sub<int> obj1;
    obj1.sub_a = 100;
    obj1.show();
    obj1.show1();
    obj1.test();

    cout << endl;
    cout << "----------" << endl;

    T_sub<string> obj2;
    obj2.sub_a = "hello";
    obj2.show();
    obj2.show1();
    obj2.test();


    return 0;
}

输出结果:

我是T_sub类的show();将显示变量sub_a:100
我是base类的show1();将显示变量base_a:10
我是T_sub类的test();将显示变量sub_a:100

----------
我是T_sub类的show();将显示变量sub_a:hello
我是base类的show1();将显示变量base_a:10
我是T_sub类的test();将显示变量sub_a:hello

示例3:类模板继承类模板

#include <iostream>

using namespace std;

template<typename T1>
class T_Base{
public:
    T1 data1;
    void show(){
        cout << "我是类模板T_Base中的成员函数show();将显示变量data1的值:" << data1 << endl;
    }
};


template <class T1, typename T2>
class T_sub:public T_Base<T1>{      //继承类模板T_Base
public:
    T2 data2;
    void print(){
        cout << "我是类模板T_sub中的成员函数print();将显示变量data2的值:" << data2 << endl;
    }

};


int main(){
    T_sub<int, string> obj1;         //这主意这里的不同
    obj1.data1 = 10;
    obj1.data2 = "hello";
    obj1.show();
    obj1.print();

    cout << endl;
    cout << "----------" << endl;

    T_sub<char, double> obj2;         //这主意这里的不同
    obj2.data1 = 'A';
    obj2.data2 = 3.14159;
    obj2.show();
    obj2.print();

    return 0;
}

输出结果:

我是类模板T_Base中的成员函数show();将显示变量data1的值:10
我是类模板T_sub中的成员函数print();将显示变量data2的值:hello

----------
我是类模板T_Base中的成员函数show();将显示变量data1的值:A
我是类模板T_sub中的成员函数print();将显示变量data2的值:3.14159

向量(vertor矢量)与泛型算法

以下是一个使用C++向量(vector)和泛型编程的示例:

#include <iostream>
#include <vector>
#include <algorithm>

template<typename T>
void printVector(const std::vector<T>& vec) {
    for (const auto& element : vec) {
        std::cout << element << " ";
    }
    std::cout << std::endl;
}

int main() {
    std::vector<int> numbers { 5, 2, 8, 1, 9 };

    // 向量中的元素排序
    std::sort(numbers.begin(), numbers.end());

    // 输出排序后的向量
    std::cout << "Sorted numbers: ";
    printVector(numbers);

    // 向量中是否包含特定元素
    int target = 8;
    bool contains = std::find(numbers.begin(), numbers.end(), target) != numbers.end();
    std::cout << "Does the vector contain " << target << "? " << (contains ? "Yes" : "No") << std::endl;

    // 向量中的元素求和
    int sum = 0;
    for (const auto& number : numbers) {
        sum += number;
    }
    std::cout << "Sum of numbers: " << sum << std::endl;

    return 0;
}

在上述示例中,我们使用了 <vector> 头文件中的向量(vector)容器和 <algorithm> 头文件中的泛型算法。首先,我们创建了一个整型向量 numbers,并初始化了一些元素。然后,我们使用 std::sort 算法对向量中的元素进行排序,使用 printVector 泛型函数打印排序后的向量。接下来,我们使用 std::find 算法判断向量中是否包含特定元素,并输出结果。最后,我们通过遍历向量的元素求和,并输出结果。

这个示例展示了向量的基本操作(排序、查找、遍历)以及泛型编程的使用。通过使用泛型编程,我们可以编写通用的函数和算法,适用于不同类型的容器和数据。

多态与虚函数

多态的基本概念

多态分为编译时多态和运行时多态;

  • 编译时多态主要是指函数的重载 (包括运算符的重载) 。对重载函数的调用,在编译时就可以根据实参确定应该调用哪个函数,因此称为编译时多态。编译阶段的多态称为静态多态。
  • 运行时多态则和继承、虚函数等概念有关。

我们这里提及的多态主要是指运行时多态。运行阶段的多态称为动态多态。

程序编译阶段都早于程序运行阶段,所以静态绑定称为早绑定,动态绑定称为晚绑定。静态多态和动态多态的区别,只在于在什么时候将函数实现和函数调用关联起来,是在编译阶段还是在运行阶段,即函数地址是早绑定的还是晚绑定的。 在类之间满足赋值兼容的前提下,实现动态绑定必须满足以下两个条件:

1)必须声明虚函数2)通过基类类型的引用或者指针调用虚函数

C++中的多态是面向对象编程的一个重要概念,指的是通过基类指针或引用调用派生类对象的成员函数时,根据实际对象的类型来决定调用哪个类中的成员函数。 实现多态的核心机制是基于虚函数(virtual function)。在基类中声明虚函数,并在派生类中进行重写(覆盖)。当通过基类指针或引用调用虚函数时,会根据实际对象的类型来动态绑定到正确的函数实现。 下面是一个简单的示例代码:

#include <iostream>

class Animal {
public:
    virtual void makeSound() {
        std::cout << "Animal makes sound!" << std::endl;
    }
};

class Dog : public Animal {
public:
    void makeSound() override {
        std::cout << "Dog barks!" << std::endl;
    }
};

class Cat : public Animal {
public:
    void makeSound() override {
        std::cout << "Cat meows!" << std::endl;
    }
};

int main() {
    Animal* animal1 = new Dog();
    Animal* animal2 = new Cat();

    animal1->makeSound();  // 实际调用的是 Dog 类的 makeSound() 函数
    animal2->makeSound();  // 实际调用的是 Cat 类的 makeSound() 函数
    
    delete animal1;
    delete animal2;
    
    return 0;

}

上述代码中,Animal 是基类,Dog 和 Cat 是继承自 Animal 的派生类。其中,Animal 类中的 makeSound() 函数被声明为虚函数,并在派生类中进行了重写。在主函数中,通过 Animal 指针指向 Dog 和 Cat 对象,并分别调用 makeSound() 函数。 由于 makeSound() 函数在基类中被声明为虚函数,因此在运行时会根据实际对象的类型进行动态绑定,从而实现多态。结果输出将根据实际对象的类型而不同,即调用了正确的派生类的成员函数。 这种多态性使得程序更加灵活和可扩展,可以通过基类指针或引用来处理多个派生类对象,而无需关心具体对象的类型,只需要调用适当的虚函数即可。

虚函数

基本概念

所谓“虚函数”,就是在函数声明时前面加了virtual关键字的成员函数。

virtual关键字只在类定义中的成员函数声明处使用,不能在类外部写成员函数体时使用。

静态成员函数不能是虚函数。

包含虚函数的类称为“多态类” 声明虚函数成员的一般格式如下

virtual 函数返回值类型 函数名(形参表)

在类的定义中使用virtual关键字来限定的成员函数即成为虚函数。再次强调一下,虚函数的声明只能出现在类定义中的函数原型声明时,不能在类外成员函数实现的时候。

派生类可以继承基类的同名函数,并且可以在派生类中重写这个函数。如果不使用虚函数,当使用派生类对象调用这个函数,且派生类中重写了这个函数时,则调用派生类中的同名函数,即“隐藏”了基类中的函数。

当然,如果还想调用基类的函数,只需在调用函数时,在前面加上基类名及作用域限定符即可。

关于虚函数,有以下几点需要注意: 1)虽然将虚函数声明为内联函数不会引起错误,但因为内联函数是在编译阶段进行静态处理的而对虚函数的调用是动态绑定的,所以虚函数一般不声明为内联函数。

2)派生类重写基类的虚函数实现多态,要求函数名、参数列表及返回值类型要完全相同。

3)基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性。

4)只有类的非静态成员函数才能定义为虚函数,静态成员函数和友元函数不能定义为虚函数。

5)如果虚函数的定义是在类体外,则只需在声明函数时添加virtual关键字,定义时不加virtual关键字。

6)构造函数不能定义为虚函数。最好也不要将operator=定义为虚函数,因为使用时容易混淆。

7)不要在构造函数和析构函数中调用虚函数。在构造函数和析构函数中,对象是不完整的,可能会出现未定义的行为。

8)最好将基类的析构函数声明为虚函数。

通过基类指针实现多态

声明虚函数后,派生类对象的地址可以赋值给基类指针,也就是基类指针可以指向派生类对象。对于通过基类指针调用基类和派生类中都有的同名、同参数表的虚函数的语句,编译时系统并不确定要执行的是基类还是派生类的虚函数;而当程序运行到该语句时,如果基类指针指向的是一个基类对象则调用基类的虚函数;如果基类指针指向的是一个派生类对象,则调用派生类的虚函数。

示例:

#include <iostream>

using namespace std;

class A{
public:
    int a = 10;
    void aaa(){
        cout << "我是基类A中的aaa()函数" << endl;
    }

    void test(){
        cout << "我是基类A中的test()函数" << endl;
    }

    void heihei(){
        cout << "我是基类A中的heihei()函数" << endl;
    }

    virtual void wawa(){
        cout << "我是基类A中的wawa()函数" << endl;
    }

    virtual void haha(){       //基类中的虚函数不能省略virtual
        cout << "我是基类A中的haha()函数" << endl;
    }

    virtual void haha1(int x){
        cout << "我是基类A中的haha1(int x)函数" << endl;
    }

};



class B:public A{
public:
    int b = 20;
    void bbb(){
        cout << "我是派生类B中的bbb()函数" << endl;
    }

    void test(){
        cout << "我是派生类B中的test()函数" << endl;
    }

    void heihei(int x){
        cout << "我是派生类B中的heihei(int x)函数" << endl;
    }

    void wawa(int x){
        cout << "我是派生类B中的wawa(int x)函数" << endl;
    }

    void haha(){
        cout << "我是派生类B中的haha()函数" << endl;
    }

    void haha1(int x){
        cout << "我是派生类B中的haha1(int x)函数" << endl;
    }

};



class C:public A{
public:
    virtual void haha(){    //派生类中与基类构成多态的函数可以省略virtual;但是强烈建议不要省略
        cout << "我是派生类C中的haha()函数" << endl;
    }

    void haha1(int x){        //派生类中与基类构成多态的函数可以省略virtual;但是强烈建议不要省略
        cout << "我是派生类C中的haha1(int x)函数" << endl;
    }
};


int main(){
    A obja;
    B objb;

    obja.aaa();
    //obja.bbb();         //不能使用派生类函数
    obja.test();
    obja.heihei();
    obja.wawa();
    obja.haha();
    obja.haha1(1);
    cout << "\n \n" << endl;

    objb.aaa();         //可以使用继承自父类的函数
    objb.bbb();
    objb.test();
    //objb.heihei();    //非法,只能使用派生类的成员函数
    //objb.wawa();      //非法,只能使用派生类的成员函数;这里要注意,父类的虚函数要和派生类的函数构成多态,则必须两者名字相同参数一致。
    objb.heihei(1);     //只能使用派生类的成员函数
    objb.wawa(1);     //只能使用派生类的成员函数
    objb.haha();
    objb.haha1(1);
    cout << "\n \n" << endl;


    A* pa = nullptr;
    B* pb = nullptr;
    pa = &objb;
    //pb = &obja;        //非法;不能将父类对象的地址赋予派生类类型的指针变量
    pa->aaa();
    //pa.bbb();            //非法;不能使用基类中没有申明过的函数
    pa->test();
    pa->heihei();
    //pa->heihei(1);            //非法;不能使用基类中没有申明过的函数(参数清单也要匹配)
    pa->wawa();      //因虚函数的参数列表不一致,没有构成多态
    //pa->wawa(1);            //非法;不能使用基类中没有申明过的函数(参数清单也要匹配)
    pa->haha();      //多态性质,使用了派生类B的haha()函数
    //pa->haha1();            //非法;不能使用基类中没有申明过的函数(参数清单也要匹配)
    pa->haha1(1);    //多态性质,使用了派生类B的haha(int x)函数
    cout << "\n \n" << endl;


    A* pc = new C;
    pc->aaa();         //继承自类A
    pc->test();         //继承自类A
    pc->heihei();         //继承自类A
    pc->wawa();         //继承自类A;在类A中wawa()是虚函数,但是在派生类C中没有重新定义,因此知识会简单继承过来。
    pc->haha();         //多态性质,在类A中haha()是虚函数,但是在派生类C中进行了重新定义,且因为A类类型的指针变量pc实际指向的是C类的对象;
    //因此,根据多态的性质,这里会调用C类对象的haha()函数。
    pc->haha1(1);       //同上,这里强调要构成多态:父类定义的虚函数和和派生类重新定义的函数在名字和参数列表上必须完全一致。

    delete pc;

    return 0;
}

输出结果:

我是基类A中的aaa()函数
我是基类A中的test()函数
我是基类A中的heihei()函数
我是基类A中的wawa()函数
我是基类A中的haha()函数
我是基类A中的haha1(int x)函数



我是基类A中的aaa()函数
我是派生类B中的bbb()函数
我是派生类B中的test()函数
我是派生类B中的heihei(int x)函数
我是派生类B中的wawa(int x)函数
我是派生类B中的haha()函数
我是派生类B中的haha1(int x)函数



我是基类A中的aaa()函数
我是基类A中的test()函数
我是基类A中的heihei()函数
我是基类A中的wawa()函数
我是派生类B中的haha()函数
我是派生类B中的haha1(int x)函数



我是基类A中的aaa()函数
我是基类A中的test()函数
我是基类A中的heihei()函数
我是基类A中的wawa()函数
我是派生类C中的haha()函数
我是派生类C中的haha1(int x)函数

通过基类引用实现多态

示例:

#include<iostream>

using namespace std;

class A {
public:
    virtual void Print() {  // 虚函数
        cout << "A::Print" << endl;
    }
};

class B : public A { // 公有派生
public:
    virtual void Print() {  // 虚函数
        cout << "B::Print" << endl;
    }
};

void PrintInfo(A &r) {
    r.Print();  // 多态,使用基类引用调用哪个Print()取决于r引用了哪个类的对象
}

int main() {
    A a;
    B b;
    PrintInfo(a);  // 使用基类对象,调用基类中的函数,输出A::Print
    PrintInfo(b);  // 使用基类对象,调用派生类中的函数,输出B::Print
    return 0;
}

多态的实现原理

多态的关键在于通过基类指针或引用调用一个虚函数时,编译阶段不能确定到底调用的是基类还是派生类的函数,运行时才能确定。 派生类对象占用的存储空间大小,等于基类成员变量占用的存储空间大小加上派生类对象自身成员变量占用的存储空间大小。

虚析构函数

如果一个基类指针指向的对象是用new运算符动态生成的派生类对象,那么释放该对象所占用的空间时,如果仅调用基类的析构函数,则只会完成该析构函数内的空间释放,不会涉及派生类析构函数内的空间释放,容易造成内存泄漏。声明虚析构函数的一般格式如下:

virtual ~类名( );

虚析构函数没有返回值类型,没有参数,所以它的格式非常简单。

如果一个类的析构函数是虚函数,则由它派生的所有子类的析构函数也是虚析构函数。使用虚析构函数的目的是为了在对象消亡时实现多态。

一般来说,一个类如果定义了虚函数,那么应该将其析构函数也定义为虚函数。构造函数不能是虚函数。

纯虚函数和抽象类

纯虚函数

纯虚函数的作用相当于一个统一的接口形式,表明在基类的各派生类中应该有这样的一个操作,然后在各派生类中具体实现与本派生类相关的操作。

纯虚函数是声明在基类中的虚函数,没有具体的定义,而由各派生类根据实际需要给出各自的定义。 声明纯虚函数的一般格式如下:

virtual 函数类型 函数名(参数表)=0;

例如:

virtual void fun()=0;

纯虚函数没有函数体,参数表后要写“=0”。派生类中必须重写这个函数按照纯虚函数名调用时,执行的是派生类中重写的语句,即调用的是派生类中的版本。

抽象类

包含纯虚函数的类称为抽象类。

因为抽象类中有尚未完成的函数定义,所以它不能实例化一个对象。

抽象类的派生类中,如果没有给出全部纯虚函数的定义,则派生类继续是抽象类。

直到派生类中给出全部纯虚函数定义后,它才不再是抽象类,也才能实例化一个对象。

虽然不能创建抽象类的对象,但可以定义抽象类的指针和引用。

这样的指针和引用可以指向并访问派生类的成员,这种访问具有多态性。

纯虚函数不同于函数体为空的虚函数,它们的不同之处如下

1)纯虚函数没有函数体,而空的虚函数的函数体为空。

2)纯虚函数所在的类是抽象类,不能直接进行实例化;而空的虚函数所在的类是可以实例化的。

它们共同的特点是纯虚函数与函数体为空的虚函数都可以派生出新的类,然后在新类中给出虚函数的实现,而且这种新的实现具有多态特征。

示例:

#include<iostream>

using namespace std;

class A {
private:
    int a;
public:
    virtual void print() = 0;   // 纯虚函数
    void func1() {
        cout << "func1" << endl;
    }
};

class B : public A {
public:
    void print();

    void func1() {
        cout << "B_func1" << endl;
    }
};

void B::print() {
    cout << "B_Print" << endl;
}

int main() {
    // A a; // 错误,抽象类不能实例化
    // A *p = new A;    // 错误,不能创建类A的实例
    // A b[2];  // 错误,不能声明抽象类的数组
    A *pa;  // 正确,可以声明抽象类的指针
    A *pb = new B;  // 使用基类指针指向派生类对象
    pb->print();    // 调用的是类B中的函数,多态,输出B——print
    B b;
    A *pc = &b;
    pc->func1();    // 因为不是虚函数,调用的是类A中的函数,输出func1
    return 0;
}

虚基类

虚基类(virtual base class)是在多重继承中使用的一个概念。当一个派生类从两个或更多的基类继承,并且这些基类之间存在公共的基类时,为了避免派生类对公共基类进行多次复制,可以使用虚基类。

通过声明一个类为虚基类,可以确保在多重继承中只有一份共享的基类子对象,从而避免了数据成员冗余和二义性问题。

定义虚基类的一般格式如下:

class 派生类名: virtual 派生方式 基类名
{
	派生类体;
};

例如,图6-3所示的各类的继承关系如下: class A

class B : virtual public A

class C : virtual public A

class D : public B, public C

示例:

#include<iostream>

using namespace std;

class A {
public:
    int a;

    void showa() {
        cout << "a=" << a << endl;
    }
};

class B : virtual public A { // 对A进行了虚继承
public:
    int b;
};

class C : virtual public A { // 对A进行了虚继承
public:
    int c;
};

class D : public B, public C {
    // 派生类D的俩个基类B、C具有共同的基类A
    // 采用了虚继承,从而使类D的对象中只包含着类A的1个实例
public:
    int d;
};

int main() {
    D Dobj; // 说明派生类D的对象
    Dobj.a = 11;    // 若不是虚继承,此行会出错!因为 "D::a" 具有二义性(A->B->D和A->C->D)
    Dobj.b = 22;    // 若不是虚继承,下面行会出错!因为 "D::showa" 具有二义性
    Dobj.showa();   // 输出 a=11
    cout << "Dobj.b=" << Dobj.b << endl;    // 输出Dobj.b=22
    return 0;
}

示例:

#include <iostream>

using namespace std;

class A{
public:
    int a = 10;
    int x = 10;
    int aa = 10;

    virtual void test(){
        cout << "我是Class A的test()函数。" << endl;    
    }

};

class B:public A{
public:
    int a = 20;
    int bb = 20;

    virtual void test(){
        cout << "我是Class B的test()函数。" << endl;    
    }

};

class C:public A{
public:
    int a = 30;
    int cc = 30;

    virtual void test(){
        cout << "我是Class C的test()函数。" << endl;    
    }

};

class D:public B,public C{
public:
    int a = 40;
    int dd = 40;
};


class E:virtual public A{      //这里和class B/C不同。(public省略则默认是private)
public:
    int a = 50;
    int x = 50;
    int ee = 50;

    virtual void test(){
        cout << "我是Class E的test()函数。" << endl;   
    }
};


class F:virtual public A{      //这里和class B/C不同。(public省略则默认是private)
public:
    int a = 60;
    int x = 60;
    int ff = 60;

    virtual void test(){
        cout << "我是Class F的test()函数。" << endl;   
    }
};


class G:public E,public F{
public:
    int a = 70;
    int gg = 70;

    //若么有下段代码,则class的定义就会报错;因为编译时不能确定A/E/F中同名函数到底要使用哪个?(相当于为同名函数指定默认值/动作)
    void test() override {     //类G中重复继承了虚基类A导致冲突,因此编译器无法确定要调用的具体虚函数版本。为了解决这个问题,可以使用作用域解析运算符来明确指定要调用的函数版本。
        A::test();  // 明确指定调用基类A的test函数
        //或者:E::test();
        //或者:F::test();
        
        //根据实际需要明确指定
    }
};


int main(){
    D obj1;
    cout << "类D的变量obj1.a:" << obj1.a << endl;
    //cout << "类D的变量obj1.aa:" << obj1.aa << endl;      //具有二义性;错误用法
    cout << "类D的变量obj1.bb:" << obj1.bb << endl;
    cout << "类D的变量obj1.cc:" << obj1.cc << endl;
    //cout << "类D的函数obj1.test():" << endl;
    //obj1.test();    //具有二义性;错误用法


    G obj2;
    cout << "类G的变量obj2.a:" << obj2.a << endl;
    //cout << "类G的变量obj2.x:" << obj2.x << endl;      //具有二义性;错误用法
    cout << "类G的变量obj2.x:" << obj2.A::x << endl;     //规避二义性;正确用法
    cout << "类G的变量obj2.x:" << obj2.E::x << endl;     //规避二义性;正确用法
    cout << "类G的变量obj2.x:" << obj2.F::x << endl;     //规避二义性;正确用法
    cout << "类G的变量obj2.aa:" << obj2.aa << endl;
    cout << "类G的变量obj2.ee:" << obj2.ee << endl;
    cout << "类G的变量obj2.ff:" << obj2.ff << endl;
    cout << "类G的变量obj2.gg:" << obj2.gg << endl;
    cout << "[默认]类G的函数obj2.test():" << endl;
    obj2.test();
    cout << "[指定A]类G的函数obj2.A::test():" << endl;
    obj2.A::test();
    cout << "[指定E]类G的函数obj2.E::test():" << endl;
    obj2.E::test();
    cout << "[指定F]类G的函数obj2.F::test():" << endl;
    obj2.F::test();


    return 0;
}

输出结果:

类D的变量obj1.a:40
类D的变量obj1.bb:20
类D的变量obj1.cc:30
类G的变量obj2.a:70
类G的变量obj2.x:10
类G的变量obj2.x:50
类G的变量obj2.x:60
类G的变量obj2.aa:10
类G的变量obj2.ee:50
类G的变量obj2.ff:60
类G的变量obj2.gg:70
[默认]类G的函数obj2.test():
我是Class A的test()函数。
[指定A]类G的函数obj2.A::test():
我是Class A的test()函数。
[指定E]类G的函数obj2.E::test():
我是Class E的test()函数。
[指定F]类G的函数obj2.F::test():
我是Class F的test()函数。

容易混淆的几个概念

virtual 和 override 功能区别

在 C++ 中,virtualoverride 是用于实现多态性的关键字,它们有以下区别:

  1. 关键字用法:
  • virtual:用于基类的成员函数声明中,表示该函数是一个虚函数,可被派生类重写。
  • override:用于派生类的成员函数声明中,显式地表明该函数覆盖了基类中的虚函数。
  1. 功能作用:
  • virtual:指示某个函数为虚函数,可以被动态绑定(动态多态性),在运行时根据对象的实际类型来决定调用具体的函数实现。这允许程序在运行时正确地选择适当的函数实现,提供了多态性的特性。
  • override:在派生类中用来说明当前函数重写了基类中的虚函数,并进行编译期检查。如果派生类中的函数使用了 override 关键字,但实际情况并不是重写基类中的虚函数,则会产生编译错误,帮助排除潜在的问题。
  1. 使用限制:
  • virtual:可以在基类中和派生类中同时使用,通过其基类指针或引用调用虚函数。
  • override:只能在派生类中使用,并用于覆盖基类中的虚函数。
  1. 使用场景不同:
  • virtual:用于在基类中声明虚函数,在派生类中可以进行重写,实现动态绑定和多态性。通过在基类指针或引用调用虚函数时,根据实际对象类型来确定调用的是基类的函数还是派生类的函数。
  • override:用于在派生类中标记对基类虚函数的重写,明确表明意图并帮助进行编译时的类型检查。如果使用了override关键字但实际上没有重写基类的虚函数,编译器会报错。
  1. 语法要求不同:
  • virtual:在基类中声明虚函数时需要使用virtual关键字。派生类中重写虚函数时,可以选择加上virtual关键字,但并非必需。
  • override:在派生类中显式声明对基类虚函数的重写时,必须使用override关键字。

下面是一个示例来说明 virtualoverride 的使用:

class Base {
public:
    virtual void print() {         // 基类中的虚函数
        cout << "Base::print()" << endl;
    }
};

class Derived : public Base {
public:
    void print() override {        // 派生类中重写基类的虚函数
        cout << "Derived::print()" << endl;
    }
};

int main() {
    Base* basePtr = new Derived();
    basePtr->print();   // 调用派生类的虚函数

    delete basePtr;
    return 0;
}

上述代码中,Base 类中的 print() 函数被声明为虚函数,Derived 类中使用 override 关键字显式标识对其的重写。在 main() 函数中,使用基类指针指向派生类对象,并通过该指针调用 print() 函数,实现了多态性。

输出结果为:

Derived::print()

可以看到,在运行时,根据对象的实际类型来调用合适的函数实现,实现了多态性和动态绑定。

总结起来,virtual 关键字用于基类的虚函数声明,override 关键字用于派生类中显式标识覆盖基类的虚函数。它们共同配合实现了C++中的多态性。

override与类继承过程中同名函数隐藏的关系

在派生类中如果定义了与基类相同名称的函数,该函数默认会隐藏(隐藏规则也被称为名字遮蔽)。这意味着在派生类对象上调用该函数时,将只会调用派生类中的函数,而不是基类中的函数。

然而,这种行为并不等同于对基类虚函数的重写。如果想要实现对基类虚函数的重写,需要在派生类中使用 override 关键字来显式声明。

使用 override 关键字的好处是编译器会在编译时对函数进行检查,确保它确实是重写了基类中的虚函数。如果派生类中的函数使用了 override 关键字,但实际情况并不是重写基类中的虚函数,则会产生编译错误,帮助排除潜在的问题。

换句话说,使用 override 关键字提供了一种显式的方式来确保代码的正确性和可读性,可以预防由于命名错误或其他原因导致的意外行为。

因此,虽然派生类对基类的重名函数会默认隐藏基类函数,但为了明确地表明对基类虚函数的重写,最好使用 override 关键字。这样做可以减少程序出错的可能性,并提高代码的可维护性和可读性。

隐藏与重写

隐藏(hiding)和重写(overriding)是面向对象编程中用于描述派生类中函数与基类中函数之间的关系的两个概念。

  1. 隐藏(Hiding):
  • 当派生类中定义了与基类同名的非虚函数或静态函数时,派生类中的函数将隐藏(hide)基类中的函数。
  • 隐藏是在编译时决定的,通过名称查找来确定调用哪个函数。
  • 编译器根据调用表达式的静态类型(即变量类型)来决定使用哪个函数,不考虑动态类型(运行时实际对象类型)。
  • 在函数隐藏的情况下,不论变量的动态类型是基类还是派生类,总是调用对应静态类型所属类的函数。
  • 隐藏适用于非虚函数和静态函数,不需要使用 virtualoverride 关键字进行声明。
  1. 重写(Overriding):
  • 重写是基于继承关系,在派生类中对基类虚函数进行改写的行为。
  • 派生类中的函数必须与基类中的同名虚函数具有相同的参数列表、返回类型和 const 属性。
  • 重写是在运行时决定的,根据对象的动态类型来决定调用哪个函数。
  • 在函数重写的情况下,如果对象的动态类型是派生类,则会调用派生类中重写的虚函数;如果对象的动态类型是基类或其他派生类,则仍然调用基类中的虚函数。
  • 重写需要通过在派生类中使用 override 关键字显式声明,以确保正确性,并允许编译器进行检查。

总结:

  • 隐藏是指派生类中的同名函数隐藏了基类中的函数,根据静态类型来决定调用哪个函数。
  • 重写是指在派生类中对基类的虚函数进行改写,根据动态类型来决定调用哪个函数。
  • 隐藏适用于非虚函数和静态函数,不需要关键字声明;重写适用于虚函数,需要使用 virtualoverride 关键字声明。

重写与多态

关系

重写(overriding)是实现多态性(polymorphism)的一种重要机制。 多态性指的是同一类型的对象在不同的情况下可以表现出不同的行为。

在面向对象编程中,通过将基类的函数声明为虚函数,在派生类中对其进行重写,从而实现多态性。当使用基类指针或引用指向派生类对象时,通过调用虚函数,能够根据实际对象的类型来确定要执行的函数版本。

具体关系如下:

  • 多态性是一个概念,描述了不同对象在相同调用下表现出不同行为的特性。
  • 重写是一种实现多态性的机制,通过在派生类中重写基类的虚函数,实现了多态性的具体表现。
  • 使用多态性可以实现更灵活、可扩展和可维护的代码结构,提高代码的可复用性和可扩展性。

通过使用基类指针或引用,可以以统一的方式处理不同派生类的对象,并根据实际对象的类型调用相应的重写函数,实现了运行时动态绑定。这样做可以使程序更具弹性和适应性,减少了对特定类型的依赖性,增加了代码的扩展性和可维护性。

总结:重写是实现多态性的具体手段,通过在派生类中重写基类的虚函数,实现了在运行时根据对象类型调用相应函数的能力,进而实现多态性。

显式申明override(强烈推荐)

在实现多态时,重写(overriding)需要显式声明使用 override 关键字。(一般来说都建议显示申明override) 在派生类中,如果要对基类的虚函数进行重写,并且希望在运行时根据对象的动态类型确定调用哪个函数版本,必须在派生类中使用 override 关键字进行显式声明。

使用 override 关键字的好处有两点:

  1. 提供编译器检查:通过在派生类中使用 override 关键字,编译器可以检查是否存在与基类虚函数相匹配的函数存在。如果派生类中的函数与基类虚函数不匹配,则会产生编译错误,提前发现问题。
  2. 增加可读性和清晰性:使用 override 关键字能够明确表达该函数是对基类虚函数的重写,增加代码的可读性和清晰性。

例如:

class Base {
public:
    virtual void func();
};

class Derived : public Base {
public:
    void func() override;  // 显式使用 override 关键字声明重写函数
};

请注意,在派生类中使用 override 关键字进行声明是可选的,但它是一种良好的编码实践,可以提高代码的可读性、可维护性并避免潜在问题。建议在重写虚函数时始终使用 override 关键字进行显式声明。

非显式申明override(可以但不推荐)

在上述示例中,如果不使用 override 关键字进行显式声明,编译器仍然可以正确地将派生类的函数与基类的虚函数进行匹配,并实现重写。

在 C++ 中,即使没有使用 override 关键字,只要满足以下条件,派生类中的函数仍然会被认为是对基类虚函数的重写:

  • 函数名称和参数列表与基类的虚函数完全相同。
  • 函数特征 (函数签名) 与基类虚函数一致,包括 const 和引用限定符。
  • 派生类函数的访问权限不能比基类虚函数更严格(例如,基类虚函数是 public,则派生类的函数也应该是 public)。

因此,实际上,可以通过满足上述条件来实现函数的重写,无论是否使用 override 关键字。但使用 override 关键字可以提供一些额外的好处,如前面所提到的编译器检查和代码的可读性、清晰性。

所以,虽然在某些情况下不使用 override 关键字也能正常工作,但建议在重写虚函数时使用 override 关键字进行显式声明,以便于代码的维护和理解。

运算符重载

基本概念

运算符重载是一种在C++中定义自定义类型对于特定运算符的行为的机制。通过运算符重载,我们可以定义自己的语义操作,使自定义类型的对象能够像内置类型一样使用运算符。

以下是运算符重载的基本概念:

  1. 运算符重载是通过函数来实现的,函数名为operator后跟着要重载的运算符符号。例如,operator+用于重载加法运算符+
  2. 运算符重载可以作为成员函数或非成员函数实现。如果重载为成员函数,则第一个参数表示该运算符左侧的操作数,如果重载为非成员函数,则需要使用至少一个参数表示操作数。
  3. 有些运算符只能以成员函数进行重载,比如赋值运算符=, 下标运算符[], 成员访问运算符.和箭头运算符->
  4. 对于一元运算符(只有一个操作数的运算符),通常作为成员函数进行重载,并且不带参数。对于二元运算符(两个操作数的运算符),通常作为非成员函数进行重载,并且接受两个参数。
  5. 运算符重载函数可以返回新的对象,也可以修改当前对象并返回引用。
  6. 不是所有的运算符都可以重载,并且存在一些限制和约定,例如&&, ||, ,等运算符是无法重载的。

以下是一个示例,演示如何重载加法运算符+用于两个自定义类型的对象相加:

#include <iostream>

class MyNumber {
private:
    int value;

public:
    MyNumber(int val) : value(val) {}

    // 运算符重载:加法运算符+
    MyNumber operator +(const MyNumber& other) const {
        return MyNumber(value + other.value);
    }

    int getValue() const {
        return value;
    }
};

int main() {
    MyNumber num1(5);
    MyNumber num2(10);

    MyNumber sum = num1 + num2;  // 调用重载的加法运算符+

    std::cout << "Sum: " << sum.getValue() << std::endl;

    return 0;
}

在上述示例中,我们通过重载operator+函数来定义了两个MyNumber对象相加的行为。在operator+函数中,我们将两个对象的值相加,并返回一个新的MyNumber对象,表示它们的和。然后,在主函数中,我们创建了两个MyNumber对象,并使用+运算符将它们相加并赋值给sum对象,最后打印出结果。

这只是运算符重载的基本介绍,C++还支持重载许多其他运算符,包括关系运算符、赋值运算符、下标运算符等。可以根据需要选择合适的运算符进行重载,并根据自定义类型的语义来定义其行为。

可重载的运算符

不可重载的运算符

运算符重载示例

#include <iostream>

using namespace std;

class A{
private:
    int a = 10;   //优先级低
    int b = 20;

public:
    A(){
        a = 100;   //优先级高
        b = 200;
    }

    //类内运算符函数重载
    int operator+(A& x){    //因为+是双目运算符,因此如果定义为类成员函数则类对象本身就占了一个参数,因此只能在设定一个参数;多于1个就会报错。
    //这里的函数返回值类型当然也可以是本类;例如本示例中就是A
        return a+b;         //在只要是A类的任意两个对象在执行+运算,都会返回a+b=300
    }

    //因为-是双目运算符,但函数定义为友元函数(不是类的成员函数),因此本类示例不能默认占用参数,所以还是可以定义2个参数。
    friend int operator-(A& x, A& y);   //类外运算符函数重载(用友元函数的方法实现)

};


int operator -(A& x, A& y) {
    return x.a-x.b;    //与第二个参数完全无关哈,示例而已,领会下逻辑就行
}



int main(){
    A obj1;
    A obj2;
    int sum = obj1 + obj2;    //**sum的类型要和operator+返回值的类型一致**
    cout << "A类的两个对象相加结果为:" << sum << endl;
    cout << "\n \n" << endl;

    int diff = obj1 - obj2;   //**diff的类型要和operator-返回值的类型一致**
    cout << "A类的两个对象相减结果为:" << diff << endl;

    return 0;
}

输出结果:

A类的两个对象相加结果为:300


A类的两个对象相减结果为:-100

建议operator和要重载的运算符之间使用空格隔开;这儿是良好的编程习惯。

类型转换函数

以下是在类 A 中定义一个类型转换函数 B() 的示例:

#include <iostream>

class B {
public:
    B(int value) : data(value) {}

    void printData() {
        std::cout << "Data: " << data << std::endl;
    }

private:
    int data;
};

class A {
public:
    operator B() {
        std::cout << "Converting A to B" << std::endl;
        return B(42);
    }
};

int main() {
    A aObject;
    B bObject = aObject;  // 类型转换调用

    bObject.printData();  // 打印输出结果:Data: 42

    return 0;
}

在这个示例程序中,类 B 具有一个带参数的构造函数,用于初始化数据成员。类 A 中定义了一个类型转换函数 operator B(),该函数将类 A 转换为类 B。

在主函数中,创建了一个类 A 的对象 aObject,然后通过类型转换调用 B bObject = aObject; 将类 A 对象转换为类 B 对象,并将结果赋值给类 B 对象 bObject。

在最后输出时,调用 bObject.printData() 打印出来的结果是 "Data: 42",表示成功通过类型转换从类 A 获取到了对应的类 B 对象,并可以使用类 B 的成员函数进行操作。

请注意,在类 A 中定义类型转换函数需要慎重使用,避免产生意外的隐式类型转换和可能引起歧义的行为。

运算符重载 vs. 类型转换函数

运算符重载和类型转换函数使用相同的关键字 operator,但它们在功能和用法上有着不同的区别。

运算符重载是一种实现自定义的运算符行为的技术,允许在特定的类或结构体中重新定义已有的运算符(如加号、减号、乘号等)的操作方式。通过运算符重载,可以实现类似于内置类型的操作符行为。

以下是一个运算符重载的示例代码,将加法运算符重载为字符串连接的运算符:

#include <iostream>
#include <string>

class MyString {
public:
    std::string value;

    MyString(const std::string& str) : value(str) {}

    MyString operator+(const MyString& other) {
        return MyString(value + other.value);
    }
};

int main() {
    MyString str1("Hello");
    MyString str2(" world!");

    MyString result = str1 + str2;

    std::cout << result.value << std::endl;  // 输出结果:Hello world!

    return 0;
}

在这个示例程序中,通过重载 operator+ 运算符,使得类 MyString 的对象能够通过加号进行字符串连接操作。通过定义 MyString 类的成员函数 operator+,我们可以对两个 MyString 对象进行加法运算,将它们的值连接起来,并返回一个新的 MyString 对象。

类型转换函数(Type Conversion Function),也称为转换运算符(Conversion Operator),允许在类中定义一种将自身类型转换为另一种类型的方法。通过类型转换函数,可以实现自定义类型之间的隐式或显式转换。

以下是一个类型转换函数的示例代码,在类 A 中定义了一个将自身转换为整型的类型转换函数:

#include <iostream>

class A {
public:
    operator int() const {
        return value;
    }

private:
    int value = 42;
};

int main() {
    A aObject;
    int intValue = aObject;  // 类型转换调用

    std::cout << intValue << std::endl;  // 输出结果:42

    return 0;
}

在这个示例程序中,类 A 定义了一个类型转换函数 operator int(),该函数将类 A 对象转换为整型。当我们在主函数中将类 A 对象赋值给一个整型变量时,会触发类型转换函数的调用,将类 A 对象转换为对应的整型值,并将其赋值给整型变量。

总结来说,运算符重载允许重新定义已有的运算符的操作行为,而类型转换函数允许将一个类转换为另一种类型。虽然它们使用相同的关键字 operator,但由于功能和用法的不同,它们具有不同的语义和目的。请根据实际需求选择适合的技术来实现所需要的功能。

流类库

C++中凡是数据从一个地方传输到另一个地方的操作都是流的操作。因此一般意义下的读操作在流数据抽象中被称为 (从流中)“提取”,写操作被称为(向流中)“插入”

输入/输出流类库

基本概念

ios是抽象基类,提供输入/输出所需的公共操作,它派生出两个类istream和ostream。为了避免多重继承的二义性,从ios派生istream和ostream时均使用了virtual关键字 (虚继承)。 istream类提供了流的大部分输入操作,对系统预定义的所有输入流重载提取运算符>>。ostream类对系统预定义的所有输出流重载插入运算符<<;由istream和ostream又共同派生了iostream类。

iostream 类库主要包含以下头文件:

  1. <iostream>:该头文件是输入输出流的主要头文件,包括了对输入、输出以及流操作符的定义。
  2. <istream>:该头文件定义了用于输入流的基类 istream,以及与输入相关的函数和流操作符。
  3. <ostream>:该头文件定义了用于输出流的基类 ostream,以及与输出相关的函数和流操作符。
  4. <iomanip>:该头文件定义了一些用于格式化输出的函数和控制符,如 setw、setprecision 等。
  5. <fstream>:该头文件提供了用于文件输入输出的类和函数,包括 ifstream(用于读取文件)、ofstream(用于写入文件)和 fstream(用于读写文件)。它们都派生自 istream 和 ostream。

除了上述头文件之外,iostream 类库还可能包含其他一些支持性头文件,用于定义一些额外的类和函数。但以上列出的头文件是基本的、常用的头文件,涵盖了大部分的输入输出需求。

在C++中,标准流对象共有四个,分别是:

  1. std::cin:标准输入流对象,用于从键盘接收输入。
  2. std::cout:标准输出流对象,用于向屏幕打印输出。
  3. std::cerr:标准错误流对象,用于输出错误消息。(非缓冲错误输出流)
  4. std::clog:标准日志流对象,用于输出程序运行时的日志信息。(缓冲缓冲错误输出流)

这些标准流对象都是全局变量,属于 <iostream> 头文件提供的功能。它们可以方便地进行输入和输出操作,满足不同的需求。

通过使用这些标准流对象,您可以方便地进行输入和输出操作,如读取用户输入、显示结果或打印错误消息。例如,您可以使用 std::cin >> variable 从键盘读取输入并将其存储到变量中,使用 std::cout << value 将值输出到屏幕上。

请注意,这些标准流对象属于 <iostream> 头文件中定义的,因此在使用它们之前需要包含该头文件。

标准流与外设之间的关系:

std::cerr和std::clog的区别

std::cerrstd::clog是C++中的两个标准流对象,用于输出错误消息和日志信息。它们的主要区别在于如何处理缓冲和重定向。

  1. std::cerr
  • std::cerr 是标准错误流对象。
  • 它通常被用来输出程序运行过程中的错误信息和异常情况。
  • std::cerr 不使用缓冲,即错误消息会立即输出到设备上,而不会等待缓冲区满或换行符出现。
  • std::cerr 默认情况下将错误输出到屏幕(或控制台)上。
  1. std::clog
  • std::clog 是标准日志流对象。
  • 它通常被用来输出程序运行时的一般性信息和调试日志。
  • std::clog 使用缓冲,默认情况下会在换行符出现或缓冲区满时刷新输出。
  • std::clog 的输出可以通过重定向进行配置,可以将日志输出到文件或其他设备上。

总结: std::cerr主要用于输出错误消息,不使用缓冲,输出立即显示,并默认输出到屏幕上。std::clog主要用于输出日志信息,使用缓冲,并具有重定向输出的能力。

需要注意的是,在一些系统上,std::cerrstd::clog可能实际上是相同的流对象,具体行为可能因编译器和操作系统而异。

输出重定向

#include <iostream>

using namespace std;

int main(){
    int x, y;
    cout << "请输入2个数字x y:" << endl;
    cin >> x >> y;
    freopen("test_std_out01.txt", "w", stdout);  //不能使用单引号(汗...);  freopen()=file reopen
    if(y == 0){
        cerr << "除数y不能为0." << endl;
    }else{
        cout << "x/y=" << x << "/" << y << "=" << x/y << endl;   //不会输出到屏幕,而是输出到"test_std_out01.txt"
    }


    return 0;
}

输出结果:

请输入2个数字x y:
100 10

cout的结果会被重定向到test_std_out01.txt文件中

x/y=100/10=10

若输入y的值是0的情况下;error信息会被输出的屏幕。

输入重定向

程序示例:

#include <iostream>

using namespace std;

int main(){
    int x, count, sum = 0;
    freopen("test_infile_01.txt", "r", stdin);     // 将标准输入重定向到"test_infile_01.txt";freopen()=file reopen
    for(count = 0; count < 10; count++){           // 循环10次;
        cin >> x;                                  // 从标准输入中读取数据并赋予变量x;
        if(cin.eof()){                             // 判断标准输入是否已经到了文件结尾(空行);
            break;
        }else{
            sum = sum + x;
        }
    }
    cout << "test_infile_01.txt文件中的前10个数之和是:" << sum << endl;

    return 0;
}

输入文件的内容:

5
10
15
20

输出内容:

test_infile_01.txt文件中的前10个数之和是:50

cout.put() 函数

cout.put() 函数是 C++ 中用于向标准输出流(cout)输出单个字符的函数。

cout.put() 函数接受一个字符作为参数,并将该字符输出到标准输出流中。它通常用于输出特定的字符,例如换行符、制表符等。

以下是一个示例,展示了使用 cout.put() 函数输出字符的功能:

#include <iostream>

int main() {
    char ch = 'A';

    std::cout.put(ch); // 输出字符 'A'
    std::cout.put('\n'); // 输出换行符

    return 0;
}

控制I / O格式-流操纵符

C++ 中的流操纵符(stream manipulators)是一种用于控制输入和输出流行为的特殊标记。它们能够修改流的状态或格式,并影响后续数据的读取或写入方式。以下是几个常见的流操纵符:

  1. std::setw(int n):设置字段宽度,用于控制输出的字段宽度为 n 个字符,不足时进行填充。
  2. std::setprecision(int n):设置浮点数输出精度,指定小数部分的位数。
  3. std::setfill(char c):设置填充字符,用于在字段宽度不足时进行填充,默认为空格字符。
  4. std::leftstd::right:设置对齐方式,std::left 左对齐输出,std::right 右对齐输出。
  5. std::boolalpha:以字符串形式输出布尔值,将 true 输出为 "true",false 输出为 "false"。
  6. std::hexstd::octstd::dec:控制整数的输出进制,分别表示十六进制、八进制和十进制。

这些流操纵符可以通过与输出流对象结合使用,例如:

#include <iostream>
#include <iomanip>

int main(){
    int x = 888;
    double pi = 3.1415926;

    std::cout << x << std::endl;
    std::cout << std::setw(8) << x << std::endl;      //设置输出宽度为8个字符;不足位数默认使用空格填充。
    std::cout << std::setw(8) << std::setfill('c') << x << std::endl;     //setfile()参数用单引号引起来,不能使用双引号。。。

    std::cout << std::setprecision(4) << pi << std::endl;        //设置输出内容的精度(四舍五入)

    std::cout << true << std::endl;                              //输出:1
    std::cout << std::boolalpha << true << std::endl;            //输出:true

    std::cout << x << std::endl;                                 //默认十进制
    std::cout << std::oct << x << std::endl;                     //八进制
    std::cout << std::dec << x << std::endl;                     //十进制
    std::cout << std::hex << x << std::endl;                     //十六进制

    std::cout << std::setbase(2) << x << std::endl;                     //错误用法,仍输出十进制
    std::cout << std::setbase(8) << x << std::endl;                     //八进制
    std::cout << std::setbase(10) << x << std::endl;                     //十进制
    std::cout << std::setbase(16) << x << std::endl;                     //十六进制


    return 0;
}

输出:

888
     888
ccccc888
3.142
1
true
888
1570
888
378
888
1570
888
378

通过灵活使用流操纵符,可以对输入和输出的格式进行精细控制,提高程序的可读性和用户体验。

常用的额cin成员函数

get()

读取输入流中的一个字符。

cin.getline()

读取输入流中的一整行数据。

cin.getline() VS. cin >> x;

cin.getline()cin >> x 是 C++ 中用于从标准输入流读取数据的两种不同方法,它们有以下区别:

  1. 读取方式:
  • cin.getline():以行为单位读取输入,并将整行字符串(包括空格)存储到指定的字符数组或字符串对象中。可以读取含有空格的字符串。
  • cin >> x:以空白字符(空格、制表符、换行符)为分隔符,依次读取输入并将值存储到变量 x。不包括空格前后的内容。
  1. 字符串处理:
  • cin.getline()适合读取一行完整的文本数据,例如读取用户输入的一整句话或一段文字。
  • cin >> x适合读取不含有空格的单词、数字或其他类型的值。
  1. 输入缓冲处理:
  • cin.getline():会读取输入流中的换行符,并在字符串末尾添加 null 终止符 '\0'。可以用于读取包含空格和换行的多个词语。
  • cin >> x:读取一个值后会忽略空白字符,包括换行符,使其在下一次读取时不干扰。

综上所述,使用 cin.getline() 可以读取包含空格和换行符的完整行数据,而 cin >> x 则适合读取不含空格的单词或值。根据具体的需求和输入格式,选择合适的方法来读取输入数据。

综合示例

#include <iostream>
#include <limits>

using namespace std;

int main(){
    char x;
    cout << "请输入一个字符:" << endl;
    cin >> x;
    cout << "输入的值被赋予x:" << x << endl;

    cout << "请输入一个字符:" << endl;
    //cin.ignore();                         // 忽略换行符;否则将受到上次输入缓冲中仍然有一个换行符残留在输入流中的影响。(因只能忽略换行符在第一次输入大量字符后仍会影响本次获取输入的数据)
    cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');    // 清空输入流,依赖#include <limits>.
    x = cin.get();
    cout << "输入的值被赋予x:" << x << endl;

    const int MAX_LENGTH = 100;         //存储最大字符串的长度
    char y[MAX_LENGTH];
    cout << "请输入一段话(getline()):" << endl;
    //cin.ignore();                         // 忽略换行符;否则将受到上次输入缓冲中仍然有一个换行符残留在输入流中的影响。(因只能忽略换行符在第一次输入大量字符后仍会影响本次获取输入的数据)
    cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');    // 清空输入流,依赖#include <limits>.
    cin.getline(y, MAX_LENGTH);
    cout << "输入的值被赋予y:" << y << endl;

    string z;
    cout << "请输入一段话(cin >> z):" << endl;
    cin >> z;
    cout << "输入的值被赋予z:" << z << endl;


    return 0;
}

输出结果:

请输入一个字符:
qwerty rtyu
输入的值被赋予x:q
请输入一个字符:
qwerty dfd
输入的值被赋予x:q
请输入一段话(getline()):
qwer 123#$% %^gg
输入的值被赋予y:qwer 123#$% %^gg
请输入一段话(cin >> z):
qe helllo world!
输入的值被赋予z:qe

eof()函数

C++ 输入流对象的一个成员函数,用于检测输入流是否达到文件末尾(End of File)。它返回一个 bool 值,如果输入流已经到达文件末尾,则返回 true;否则返回 false。

ignore() 函数

ignore() 函数用于忽略输入缓冲区中的字符。它可以清除输入缓冲区中的一个或多个字符,或者忽略直到指定的分隔符(delimiter)出现之前的所有字符。

在 C++ 中,ignore() 是 istream 类的成员函数之一。它接受两个参数:

  1. 第一个参数是要忽略的字符个数(可以是一个整数)。
  2. 第二个参数(可选)是分隔符字符,指示在遇到该字符之前停止忽略。

如果省略第一个参数,则默认忽略一个字符。如果省略第二个参数,则默认不使用分隔符。

以下是 ignore() 函数的几种常见用法:

#include <iostream>
#include <limits>

int main() {
    char ch;

    std::cout << "请输入一个字符:" << std::endl;
    std::cin >> ch; // 读取一个字符

    // 忽略输入缓冲区中的剩余字符
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');

    std::cout << "继续输入一个字符串:" << std::endl;
    std::string str;
    std::getline(std::cin, str); // 读取一行字符串
    std::cout << "您输入的字符串是:" << str << std::endl;

    return 0;
}

在上面的示例中,我们首先使用 std::cin 来读取一个字符,并将其存储在变量 ch 中。随后,我们调用 std::cin.ignore() 来忽略输入缓冲区中的剩余字符。

函数 ignore() 的第一个参数使用了 std::numeric_limits<std::streamsize>::max(),它表示要忽略输入缓冲区中的全部字符。第二个参数是换行符 '\n',指示在遇到换行符之前停止忽略。

接着,我们使用 std::getline() 函数来读取一行字符串,并将其存储在变量 str 中。此时,输入缓冲区已经被清空,可以安全地进行下一次输入操作。

需要注意的是,ignore() 函数通常与其他输入操作(如 >>getline() 等)搭配使用,以确保输入缓冲区没有残留的无效字符影响下一次的输入操作。

peek()函数

peek() 函数是C++标准库中流类 (istream) 的一个成员函数,用于查看输入流中的下一个字符,而不提取它。

可以简单理解为;查看当前缓冲中的第一个字符。

示例:

#include <iostream>

using namespace std;

int main(){
    char x;
    cout << "请任意输入一段话:" << endl;
    cin >> x;        //从输入中取走了第一个字符

    char y;
    y = cin.peek();   //看一眼当前缓冲中的下一个字符
    cout << "C++使用peek()函数偷看了一眼输入流,看到了:" << y << endl;
    cout << "判断看到的值是否是空值或者/n或者EOF等" << endl;
    if(y == EOF){
        //cout << "y的值是EOF。" << endl;     //这样写回出现警告:warning: character constant too long for its type
        cout << "y的值是EOF。\n";           //上一句改写成这样
    }else if (y == '\n')
    {
        cout << R"(y的值是\n。)" << endl;       //特殊字符原样输出的技巧    R"(xxxxxxxx)"
    }else if(y == ' '){
        cout << "y的值是' '。" << endl;
    }else{
        cout << "y的值猜不中。" << endl;
    }
    

    return 0;
}

第一次输出:

我输入a然后按回车。

请任意输入一段话:
a
C++使用peek()函数偷看了一眼输入流,看到了:

判断看到的值是否是空值或者/n或者EOF等
y的值是\n。

第二次输出:

我输入一段字符串;然后回车。

请任意输入一段话:
qwerty uiop[
C++使用peek()函数偷看了一眼输入流,看到了:w
判断看到的值是否是空值或者/n或者EOF等
y的值猜不中。

大家可以试着玩一下;看看输出是否和自己想的一样。

文件操作流类库

文件基本概念和文件流类

文件的概念

在C++中,文件被视为一种数据流,可以通过文件流对象来进行输入和输出操作。C++将文件看作是一个有序的字节序列,可以读取和写入数据。

在C++中,文件可以分为两种类型:文本文件和二进制文件。文本文件是由字符组成的文件,可以使用文本编辑器打开查看其内容。而二进制文件则是由二进制数据组成的文件,无法直接通过文本编辑器查看其内容。

C++中使用文件流对象来进行文件的输入和输出操作。常见的文件流对象有std::ifstream用于读取文件,std::ofstream用于写入文件,以及std::fstream用于读写文件。

通过文件流对象,可以打开文件、读取文件内容、写入文件内容、关闭文件等操作。文件流对象提供了一系列的成员函数和操作符来实现这些功能,例如open()函数用于打开文件,<<操作符用于写入数据,>>操作符用于读取数据,close()函数用于关闭文件等。

总之,C++将文件看作是一种数据流,可以通过文件流对象对其进行输入和输出操作,方便地对文件进行读写操作。

从不同的角度来看待文件就可以得到不同的文件分类:

C++根据文件数据的编码方式不同分为

  • 文本文件
  • 二进制文件

根据存取方式不同分为

  • 顺序存取文件
  • 随机存取文件

所谓“文本文件”和“二进制文件”是从文件格式的角度进行分类;是约定俗成的,从计算机用户角度出发进行的分类。

所谓的“顺序存取文件”和“随机存取文件”是根据访问文件中数据的方式来划分的。

顺序存取文件就是按照文件中数据存储次序进行顺序操作为访问第i个数据,就首先要访问第i-1个数据,在整个文件操作过程中,将移动位置指针的工作交给系统自动完成。磁带文件就是一个典型的顺序存取文件。

随机访问文件是根据应用的需要,通过命令移动位置指针直接定位到文件内需要的位置并进行数据操作。对文件的基本操作分为读文件和写文件。所谓“读文件”就是将文件中的数据读入内存之中,也称为“输入”。所谓“写文件”就是将内存中的数据存入文件之中,也称为“输出'。

C++文件流类

C++标准类库中有3个流类可以用于文件操作,这3个类统称为文件流类,分别如下:

  1. ifstream: 用于从文件中读取数据
  2. ofstream: 用于向文件中写入数据
  3. fstream: 既可用于从文件中读取数据,又可用于向文件中写入数据。

使用这3个流类时,程序中需要包含fstream头文件。

类ifstream和类fstream都是从类istream派生而来的,因此类ifstream拥有类istream的全部成员函数。

同样,类ofstream和类fstream也拥有类ostream的全部成员函数。

这3个类中有一些十分熟悉的成员函数可以使用,如operator<<operator>>peek()ignore()getline()get()等. 在程序中,要使用一个文件,必须包含3个基本步骤:

  • 打开 (open) 文件
  • 操作文件
  • 关闭 (close) 文件

操作文件就是对文件进行读/写;C++文件流类有相应的成员函数来实现打开、读、写、关闭等文件操作。

打开和关闭文件

打开:

流类名 对象名;
对象名.open(文件名, 模式);

关闭:

对象名.close();

综合示例:

#include <iostream>
#include <fstream>

using namespace std;

int main(){
    // 使用ifstream类创建对象并以读模式(ios::in)打开文件;ios::in模式支持ifstream和fstream的对象使用
    // 若文件不存在则打开出错
    ifstream obj1(".\\test_file_001.txt", ios::in);
    if(obj1){
        cout << "文件.\\test_file_001.txt  打开成功!" << endl;
        obj1.close();
    }else{
        cout << "文件.\\test_file_001.txt  打开失败!!" << endl;
    }


    // 使用ofstream类创建对象并以写模式(ios::out)打开文件;;ios::out模式支持ofstream和fstream的对象使用
    // 若文件不存在则创建;存在则清空文件
    ofstream obj2(".\\test_file_002.txt", ios::in);
    if(obj2){
        cout << "文件.\\test_file_002.txt  打开成功!" << endl;
        obj2.close();
    }else{
        cout << "文件.\\test_file_002.txt  打开失败!!" << endl;
    }



    // 使用ofstream类创建对象并以附加 模式(ios::app)打开文件;;ios::app模式支持ofstream对象使用
    // 若文件不存在则创建;若存在则打开并将光标定位到文件尾部。
    ofstream obj3(".\\test_file_003.txt", ios::in);
    if(obj3){
        cout << "文件.\\test_file_003.txt  打开成功!" << endl;
        obj3.close();
    }else{
        cout << "文件.\\test_file_003.txt  打开失败!!" << endl;
    }


    return 0;
}

输出结果:

文件.\test_file_001.txt  打开成功!
文件.\test_file_002.txt  打开成功!
文件.\test_file_003.txt  打开成功!
文件读写操作

例子:根据用户输入的信息向文本文件中写入数据。

#include <iostream>
#include <fstream>

using namespace std;

int main(){
    // 定义输入变量
    string id, name;
    int age, score;
    string filename = ".\\student.txt";
    

    // 定义输出文件的对象
    //ofstream obj(".\\student.txt", ios::out);      // 写入的数据中文乱码
    ofstream obj(filename, ios::binary);
    if(obj.is_open()){
        cout << "文件" << filename << "打开成功!" << endl;
    }else{
        cout << "文件" << filename << "打开失败!" << endl;
    }


    // 输入数据并写入数据
    cout << "请输入:学号 姓名 年龄 成绩" << endl;
    cin >> id >> name >> age >> score;
    cout << "您输入的信息分别是:    " << " 学号:" << id  << " 姓名:" << name << " 年龄:" << age << " 成绩:" << score << endl;
    //obj << u8"学号 姓名 年龄 成绩" << endl;                 // u8配合ios::binary将中文以utf-8编码写入文件
    obj << "学号 姓名 年龄 成绩" << endl;
    obj << id << ' ' << name << ' '  << age << ' '  << score << endl; 
    obj.close();



    return 0;
}

输出内容:

文件.\student.txt打开成功!
请输入:学号 姓名 年龄 成绩
1001 夏明亮 18 100
您输入的信息分别是:     学号:1001 姓名:夏明亮 年龄:18 成绩:100

例子:从文本中读取内容并显示在控制台上。

#include <iostream>
#include <fstream>

using namespace std;

int main(){
    string filename = ".\\student.txt";
    
    //将写入结果从文件中读取出来
    string content_line;
    ifstream obj1(filename, ios::in);      //只读打开
    if(obj1.is_open()){
        cout << "文件" << filename << "打开成功!" << endl;
        int i = 1;
        while(getline(obj1, content_line)){
            cout << i << "==>" << content_line << endl;
            i++;
        }
        obj1.close();
    }else{
        cout << "文件" << filename << "打开失败!" << endl;
    }
    return 0;
}

显示结果:

文件.\student.txt打开成功!
1==>学号 姓名 年龄 成绩
2==>1001 夏明亮 18 100
文本文件 VS. 二进制文件

在输入/输出过程中,系统要对内外存的数据格式进行相文本文件是以文本形式存储数据,其优点是具有较高的兼容性。缺点是存储一批纯数值信息时,要在数据之间人为地添加分隔符。应转换,文本文件的另一个缺点是不便于对数据进行随机访问。

二进制文件是以二进制形式存储数据,其优点是便于对数据实行随机访问(相同数据类型的数据所占空间的大小均是相同的,不必在数据之间人为地添加分隔符)。在输入/输出过程中,系统不需要对数据进行任何转换.缺点是数据兼容性差。

通常纯文本信息 (如字符串) 以文本文件形式存储,而将数值信息以二进制文件形式存储。

随机访问文件 VS. 顺序访问文件

如果一个文件只能进行顺序存取操作,则称为顺序文件。典型的顺序文件(设备)是键盘、显示器和保存在磁带上的文件。

如果一个文件可以在文件的任意位置进行存取操作,则称为随机文件。磁盘文件就是典型的随机文件。

在访问文件的过程中,若严格按照数据保存的次序从头到尾访问文件,则称为顺序访问。

在访问文件的过程中,若不必按照数据的存储次序访问文件,而是要根据需要在文件的不同位置进行访问,则称为随机访问。

显然,对于顺序文件只能进行顺序访问;对于随机文件既可以进行顺序访问,也可以进行随机访问。

流操纵符

基本介绍

操作符

说明

>>

流提取操作符

<<

流插入操作符

我们在C语言中已经提及过流的概念,对于各种数据向指定目标读写,提及过缓冲区的概念;类似的,在C++中我们定义了一些对象(读写的目标),定义好了对这些对象读写的方式,可以使用流操作符直接实现对流对象的读写。 最一般的iostreamfstream对应控制台输入输出流和文件操作流;还有sstream字符串操作流。 本质上是封装了C语言中各类输入输出函数(不论是对控制台的还是文件的),通过统一的操作符实现,方便我们的使用;使用时要声明相应的头文件;

#include<iostream>
#include<fstream>
using namespace std;
#include<sstream>

流操作符的左部是操作对象,右部是数据变量,操作符方向指明了数据流动的方向,写入数据变量或者读取数据变量到对象之中。 流操作中流动的数据本质上是文本字符,流操作符会完成各种基本类型到字符串类型的转换

格式化控制

以字符串形式的流操作就一定会存在格式化控制,在C++中可以通过一些格式化函数控制流中的文本格式: 头文件 #include<iomanip>

1)setw()

默认:域宽确定,右对齐,无填充。

它接受一个整数参数,表示字段的宽度(以字符为单位)。如果输出的内容长度不足指定的宽度,将会在字段的左侧填充空格字符,以确保输出的内容占据指定的宽度。

操作符

说明

对象>>setw(val)>>a

指定输入的域宽,从对象读出时最多只提取val位

对象<<setw(val)<<a

指定输出的域宽,a在对象中的存在至少占val位,超过不截断

只对相邻的一项有效

#include <iostream>
#include <iomanip>
int main() {
    int num1 = 123;
    int num2 = 456;
    std::cout << std::setw(8) << num1 << std::endl;
    std::cout << std::setw(8) << num2 << std::endl;
    return 0;
}

输出结果:

123
     456

2)setfill()

用于设置填充字符,即指定在字段宽度不足时使用的填充字符。它接受一个字符参数,表示填充字符。默认情况下,填充字符是空格字符。

#include <iostream>
#include <iomanip>
int main() {
    int num = 123;
    std::cout << std::setfill('*') << std::setw(8) << num << std::endl;
    std::cout << std::setfill('#') << std::setw(8) << num << std::endl;
    return 0;
}

输出结果:

****123
####123

3)setprecision(val)

操作符

说明

对象<<setprecision(val)<<a

指定输出的有效位数,指定的太大则规定失效

对指定之后的所有数据生效

4)setiosflags(状态标志)

状态标志

说明

ios::left

左对齐,右边填充空格

ios::right

右对齐,左边填充空格,默认

ios::fixed

以定点形式输出浮点数

ios::scientific

以科学计数法输出浮点数

ios::dec

随后输出的所有整数为十进制

ios::hex

随后输出的所有整数为十六进制

ios::oct

随后输出的所有整数为八进制

ios::showpoint

输出小数点和尾部的零

ios::showpos

输出正数前的+

ios::uppercase

十六进制使用大写字母输出

ios::basefield

配合ios::dec/ios::hex/ios::oct使用

5) setf()

cout.setf(ios::dec, ios::basefield); 是用于将输出流 cout 的输出格式设置为十进制。

ios::basefield 是一个格式控制标志,用于指定整数输出的进制。通过调用 cout.setf() 函数并传入 ios::decios::basefield 参数,可以将输出流的进制设置为十进制。

在设置了十进制输出后,后续输出整数的方式将会按照十进制进行。

ios::basefield 是 C++ 中的一个格式控制标志,用于指定整数输出的进制。

ios::basefield 可以接受三个值:

  • ios::dec:十进制(默认值)
  • ios::oct:八进制
  • ios::hex:十六进制

在使用 ios::basefield 标志后,后续输出整数的方式将会按照指定的进制进行。这个标志对于 ostream 类(如 cout)的输出操作符 << 是有效的。

综合示例1:

#include <iostream>
#include <iomanip>     //需要引入这个头文件支持
int main(){
    for(int i=0; i<=100; i++){
        std::cout << std::setw(3) << i << std::endl;  //3位;默认右对齐;默认空格填充
    }

    for(int i=0; i<=100; i++){
        std::cout << std::setw(4) << std::left << i << std::endl;  //3位;左对齐;默认空格填充
    }

    for(int i=0; i<=100; i++){
        std::cout << std::setw(3) << i << std::endl;      //3位;左对齐(延续前面的设定);默认空格填充
    }

    for(int i=0; i<=100; i++){
        std::cout << std::setfill('*') << std::setw(4) << std::right << i << std::endl;      //4位;右对齐(延续前面的设定);*填充
    }

    for(int i=0; i<=100; i++){
        std::cout << std::setw(4) << i << std::endl;      //4位;右对齐(延续前面的设定);*填充(延续前面的设定)
    }

    for(int i=0; i<=100; i++){
        std::cout << std::setfill('0') << std::setw(8) << std::right << i << std::endl;      //8位;右对齐(延续前面的设定);0填充[字符串不是int要注意]
    }

    double num = 3.14159;
    std::cout << std::fixed << std::setprecision(2) << num << std::endl;     //输出3.14

    return 0;
}

输出结果:

0
……
100
0
1
……
100
0
1
……
100
***0
***1
……
*100
***0
***1
……
*100
00000000
00000001
……
00000100
3.14

简单输入输出

流操作符最常用的操作自然是向控制台的输入和输出,在#include<iostream>中我们定义了标准输入输出类,并定义好了实例cincout,关于类的知识我们在下一节讲解,这里我们简单了解用法就好;

1.cout对象

配合<<使用,是ostream类的一个实例,表示数据流向控制台,写在命令窗口上,作为一个封装好的实例,ostream类包含了一些定义好的成员函数,同样能够实现格式化控制输出。

在输出之前设置

  • cout.width(val);
  • cout.precision(val);
  • cout.setf(flag);
  • cout.unsetf(flag);
2.cin对象

配合>>使用,是istream类的一个实例,表示控制台数据流向变量,除去类似cout的操作,istream定义的成员函数实现了一些对字符串读取:

  • cin>>a,以空白字符为间断,跳过所有的空白字符,设置域宽也不能解决空格中断。
  • cin.getline(str, size);,最多读取size-1大小的一行字符,遇到换行符结束读取,末尾添加\0。(读取但不存储\n
  • cin.get();,一次读取一个字符,包括空白字符。
  • cin.ignore(n, c);,控制忽略,cin跳过n个字符或者遇到字符c忽略它;不设参数时忽略当前缓存区的第一个字符;常用来处理>>之后剩余的换行符!!!
3.操作符

操作符可以直接跟在流操作符之后,表示特定的操作,最常用的的cout<<endl

操作符

描述

输入

输出

boolalpha

启用boolalpha标志

dec

启用dec标志

endl

输出换行标示,并清空缓冲区


ends

输出空字符


fixed

启用fixed标志


flush

清空流


hex

启用 hex 标志

internal

启用 internal 标志


left

启用 left 标志


noboolalpha

关闭boolalpha 标志

noshowbase

关闭showbase 标志


noshowpoint

关闭showpoint 标志


noshowpos

关闭showpos 标志


noskipws

关闭skipws 标志


nounitbuf

关闭unitbuf 标志


nouppercase

关闭uppercase 标志


oct

启用 oct 标志

right

启用 right 标志


scientific

启用 scientific 标志


showbase

启用 showbase 标志


showpoint

启用 showpoint 标志


showpos

启用 showpos 标志


skipws

启用 skipws 标志


unitbuf

启用 unitbuf 标志


uppercase

启用 uppercase 标志


ws

跳过所有前导空白字符


文件输入输出

类似的,在#include<fstream>中定义了文件流类型,不同于控制台流的是,并不存在标准流,所以我们需要在程序中自己定义文件流类的实例; 到目前为止,我们已经使用了 iostream 标准库,它提供了 cin 和 cout 方法分别用于从标准输入读取流和向标准输出写入流。 定义了三个新的数据类型:

数据类型

描述

ofstream

该数据类型表示输出文件流,用于创建文件并向文件写入信息。

ifstream

该数据类型表示输入文件流,用于从文件读取信息。

fstream

该数据类型通常表示文件流,且同时具有 ofstream 和 ifstream 两种功能,这意味着它可以创建文件,向文件写入信息,从文件读取信息。

#include<iostream>
using namespace std;
#include<fstream>
int main(){
	ofstream file;
}
123456
1.打开关闭文件

cin这样的实例已经绑定了标准输入,对应的我们也要为文件流绑定对应的文件,最后也要关闭这种绑定,释放资源。在C++中我们定义了成员函数实现这种操作:

  • file.open(filename, flags) ,可以以不同的权限打开文件。

模式标志

描述

ios::app

追加模式。所有写入都追加到文件末尾。

ios::ate

文件打开后定位到文件末尾。

ios::in

打开文件用于读取。

ios::out

打开文件用于写入。

ios::trunc

如果该文件已经存在,其内容将在打开文件之前被截断,即把文件长度设为 0。

  • file.close(),关闭相应的文件流!
2.文件读写、状态控制

关于文本文件读写可以直接使用流操作符实现,与向coutcin的操作是完全一致的,这也就体现了我们面向对象的好处! 同样,文件流也定义了成员函数,对于二进制文件的读写要借助于成员函数:

  • file.write((cahr*)buffer, sizeof(buffer));之后对文件的写入操作会识别相应的数据类型,以二进制写入。
  • file.read((cahr*)buffer, sizeof(buffer));之后对文件的读取操作会识别相应的数据类型,读出转化二进制数据存入相应变量。

成员函数

含义

eof()

到达文件末尾返回非零,否则返回零

总结

C++的输入输出主要通过库中已经定义的类实现,各种功能通过成员函数来实现,常用的成员函数一定要掌握,其他的在今后学习中慢慢了解即可,我们涉及到的读写控制其实是不多的/(ㄒoㄒ)/~~

fstream类的getline()函数

fstream::getline()函数是C++标准库中fstream类的一个成员函数,用于从文件中逐行读取数据。 使用方法如下:

首先创建一个fstream对象并打开文件:

#include <fstream>
using namespace std;

fstream file;
file.open("filename.txt", ios::in);

这里的filename.txt是要读取的文件名,ios::in表示以读取方式打开文件。

使用getline()函数读取文件中的一行数据:

string line;
getline(file, line);

这里的file是要读取的文件对象,line是用于存储读取到的一行数据的字符串。

循环读取文件中的每一行数据,直到文件结束:

string line;
while (getline(file, line)) {
    // 处理读取到的数据
}

在循环中可以对读取到的每一行数据进行处理,比如输出到屏幕上或者存储到一个容器中。

关闭文件:

file.close();

在读取完成后,需要调用close()函数关闭文件。 总结:fstream::getline()函数用于从文件中逐行读取数据,可以通过循环读取文件中的每一行数据,直到文件结束。

示例:

#include <iostream>
#include <fstream>
#include <string>     //可省略的
using namespace std;
int main(){
    ifstream inf;
    inf.open("00.temp.cpp", ios::in);

    if(!inf){
        cerr << "文件打开失败!" << endl;
        return 1;
    }

    string line;
    int num = 1;
    while(getline(inf, line)){
        cout << num << ":" << line << endl;
        num ++;
    }

    inf.close();
    return 0;
}

文件读写综合示例

定义一个日志管理类,实现如下功能:

(1) 将单条日志信息追加到二进制日志文件 logdat 中。日志信息包括:日志序号(整型)、日志类型(整型) 和日志内容(字符串,长度不大于 100 字节)。

(2) 计算日志文件中日志的数量。

(3) 从日志文件中读取最近的 10 条日志,显示到屏幕上。如果不足 10 条,则全部显示。

#include <iostream>
#include <fstream>
using namespace std;

class logmgr{
public:
    bool addlogtofile(int id,int type, char msg[100], string logfilepath){
        fstream file(logfilepath, ios::binary | ios::app);    //以二进制和追加的模式打开
        if(file.is_open()){
            file << id << " "<< type << " " << msg << endl;  //注意别忘了加上适当的空格
            file.close();
            return true;
        }else{
            return false;
        }
    }

    int getloglinenum(string logfilepath){
        fstream file;
        file.open(logfilepath,  ios::in);
        int linecount = 0;
        string line;
        if(file.is_open()){
            while(getline(file, line)){
                if(!file.eof()){
                    linecount++;
                }else{
                    file.close();
                }
            }
        }else{ 
            return 1; 
        }
        return linecount;
    }

    void getloglast_10(string logfilepath){
        ifstream file;
        file.open(logfilepath, ios::in);
        string line;
        if(file.is_open()){
            for(int i=0 ; i<10 && getline(file, line); i++){
                //if(!file.eof()){}
                cout << i+1 << ":" << line << endl; 
            }
            file.close();
        }else{ 
            cout << "show last 10 logs error!" << endl; 
        }
        
    }
};

int main(){
    logmgr obj;
    for(int i=1; i<=5; i++){
        obj.addlogtofile(1000+i, 01, "test msg 001", ".\\log.bat");  //这里将string传给char[]编译时会出现警告;建议换种写法。
    }
    cout << "Log file's linecount:" << obj.getloglinenum(".\\log.bat") << endl;
    obj.getloglast_10(".\\log.bat");

    return 0;
}

输出结果:

Log file's linecount:5
1:1001 1 test msg 001
2:1002 1 test msg 001
3:1003 1 test msg 001
4:1004 1 test msg 001
5:1005 1 test msg 001

输入输出流类库 & 文件流类库的继承与派生关系

在C++中,输入输出流和文件流是通过继承体系来实现的。下面是它们之间的继承派生关系:

  1. 输入输出流(I/O Stream)
  • istream:用于输入操作的基类,提供了从设备读取数据的功能。
  • ifstream:用于从文件读取数据。
  • istringstream:用于从字符串读取数据。
  • iostream:同时具有输入和输出功能。
  • cin:标准输入流对象,用于从控制台获取输入。
  • ostream:用于输出操作的基类,提供了向设备写入数据的功能。
  • ofstream:用于向文件写入数据。
  • ostringstream:用于将数据写入字符串。
  • iostream:同时具有输入和输出功能。
  • cout:标准输出流对象,用于向控制台输出。
  1. 文件流(File Stream)
  • ifstream:继承自istream,用于从文件读取数据。
  • ofstream:继承自ostream,用于向文件写入数据。
  • fstream:同时继承自ifstream和ofstream,既可以读取文件,也可以写入文件。

这个继承体系使得我们能够使用统一的编程接口来处理不同类型的输入输出操作。无论是从标准输入读取、从文件读取,还是将数据输出到标准输出或文件,我们都可以使用相应的流对象进行操作。

例如,在读取文件时,我们可以使用ifstream对象;在向文件写入数据时,我们可以使用ofstream对象;而对于既需要读取文件又需要写入文件的情况,我们可以使用fstream对象。

下面是一个简单的示例代码,展示了输入输出流和文件流之间的关系:

#include <iostream>
#include <fstream>

int main() {
    std::ifstream inputFile("input.txt");
    std::ofstream outputFile("output.txt");

    int number;
    inputFile >> number;
    outputFile << "The number is: " << number;

    inputFile.close();
    outputFile.close();

    return 0;
}

在上述代码中,我们使用ifstream对象从名为"input.txt"的文件读取数据,然后使用ofstream对象将数据写入名为"output.txt"的文件中。通过这种继承关系,我们可以方便地进行文件的读写操作。

输入流指针

在C++中,seekg()函数是用于设置输入流的读指针位置的成员函数。它可以用于ifstream、istringstream等输入流对象。[g:get] seekg()函数有两种形式:

1) seekg(offset, origin):设置读指针的位置,其中offset表示相对于origin的偏移量。

offset:表示偏移量,可以是正数或负数,用于指定移动的字节数origin:表示起始位置,可以是以下值之一:

  • std::ios::beg:起点为文件开头。
  • std::ios::cur:起点为当前位置。
  • std::ios::end:起点为文件结尾。

2) seekg(position):直接设置读指针的绝对位置。

position:表示读指针的绝对位置,以字节为单位。

以下是一个示例代码,演示如何使用seekg()函数设置输入流的读指针位置:

#include <iostream>
#include <fstream>

int main() {
    std::ifstream input("example.txt", std::ios::binary);

    if (input) {
        // 设置读指针从文件开头向后移动100字节
        input.seekg(100, std::ios::beg);
    
        // 或者直接设置读指针的绝对位置为200字节
        // input.seekg(200);
    
        // 进行后续的读取操作
    }
    else {
        std::cout << "Failed to open file." << std::endl;
    }
    
    input.close();
    
    return 0;

}

上述代码中,我们首先创建了一个ifstream对象input,并以二进制模式打开了名为example.txt的文件。然后,我们使用seekg()函数将读指针从文件开头向后移动100字节(或直接设置读指针的绝对位置为200字节)。最后,我们可以进行后续的读取操作。

请注意,seekg()函数是针对输入流的操作,如果要设置输出流的写指针位置,可以使用seekp()函数。 通过使用seekg()函数,我们可以在输入流中移动读指针到指定位置,以便进行后续的读取操作。

输出流指针

seekp()函数的作用是在C++中用于设置文件输出位置。它可以用来改变文件流对象(如ofstream)的内部指针,从而控制数据的写入位置。[p:put]

使用示例:

#include <iostream>
#include <fstream>

int main() {
    std::ofstream file("example.txt"); // 打开一个文件用于写入

    if (file.is_open()) {
        file << "Hello, World!\n"; // 写入一些数据
    
        // 使用seekp()函数将输出位置设置到文件开头
        file.seekp(0);
    
        file << "New data"; // 从文件开头开始覆盖写入新的数据
    
        file.close(); // 关闭文件
    } else {
        std::cout << "Unable to open file";
    }
    
    return 0;

}

在上面的示例中,我们首先打开一个名为"example.txt"的文件用于写入。然后,我们向文件中写入一些数据"Hello, World!\n"。接下来,我们使用seekp()函数将输出位置设置到文件开头(偏移量为0)。然后,我们再次向文件中写入数据"New data",这次会从文件开头开始覆盖写入新的数据。最后,我们关闭文件。 注意:示例中的代码只是一个简单的演示,实际使用中可能需要做更多的错误处理和逻辑控制。

内联函数

内联函数(inline function)是一种编程特性,它可以在程序编译时将函数的代码插入到调用该函数的地方,而不是通过函数调用的方式执行。这样可以减少函数调用的开销,提高程序的执行效率。 在C++中,使用关键字inline来声明内联函数。通过将函数定义放在函数声明的位置,编译器就会将函数的代码插入到调用该函数的地方,而不是在执行时通过函数调用的方式执行。 内联函数适用于函数体较短、频繁调用的函数。它在一些简单的计算、访问器函数等场景中非常有用。 以下是一个内联函数的示例:

#include <iostream>

// 声明内联函数
inline int add(int a, int b) {
    return a + b;
}

int main() {
    int result = add(3, 4); // 调用内联函数
    std::cout << "Result: " << result << std::endl;
    return 0;
}

在上面的示例中,add()函数被声明为内联函数,它将两个整数相加并返回结果。在main()函数中,我们调用了add()函数,并将结果输出到控制台。由于add()函数体很短,编译器可以将其代码插入到调用点,从而避免了函数调用的开销。

多线程

方法:

  • thread
  • pthread

区别:

pthreadstd::thread 是两种不同的多线程编程接口。

pthread 是 POSIX 线程库(Portable Operating System Interface),是一套用于支持跨平台多线程编程的标准。它提供了创建、控制和同步线程的函数和数据类型。pthread 是 C 语言的接口,并且可以在多种操作系统上使用,包括 Linux、UNIX 和 macOS。

std::thread 是 C++11 标准库中引入的线程类,它是对 pthread 的封装和增强。std::thread 提供了一个面向对象的方式来创建和管理线程,更符合 C++ 的编程风格。通过使用 std::thread,可以以更简洁和安全的方式编写多线程程序。

以下是 pthreadstd::thread 的一些区别:

  1. 语言支持:pthread 是 C 语言的接口,可以与 C++ 配合使用。而 std::thread 是 C++ 的标准库,为 C++ 提供了原生的多线程编程支持。
  2. 类型和面向对象性质:pthread 是基于函数指针的接口,线程函数必须是静态函数或全局函数。std::thread 则是一个类,线程函数可以是成员函数、Lambda 表达式或可调用对象。
  3. 可移植性:pthread 是 POSIX 标准的一部分,因此可以在多个操作系统上使用。而 std::thread 是 C++ 标准库的一部分,也可以在支持 C++11 标准的平台上使用。
  4. 使用简洁性:相比于 pthreadstd::thread 提供了更高级、更便捷的接口,使用起来更加简单明了。它隐藏了许多底层的线程管理细节,例如线程创建、入口点和参数传递等。

总体而言,如果你使用的是 C++ 并且目标平台支持 C++11 标准,推荐使用 std::thread 进行多线程编程,它提供了更现代化和方便的编程模型。如果需要在不同的操作系统以及与 C 代码进行交互时,可以选择使用 pthread 接口。

pthread库

这个简单的实例代码使用 pthread_create() 例程创建了 5 个线程。每个线程打印一个 "Hello World!" 消息,然后调用 pthread_exit() 终止线程。

#include <iostream>
// 必须的头文件是
#include <pthread.h>

using namespace std;

#define COUNT_THREADS 5

// 线程的运行函数
void* say_hello(void* args)
{
    cout << "Hello boy!" << endl;
    return nullptr;  // 返回一个空指针
}

int main()
{
    // 定义线程的 id 变量,多个变量使用数组
    pthread_t tids[NUM_THREADS];
    for(int i = 0; i < NUM_THREADS; ++i)
    {
        //参数依次是:创建的线程id,线程参数,调用的函数,传入的函数参数
        int ret = pthread_create(&tids[i], NULL, say_hello, NULL);
        if (ret != 0)
        {
           cout << "pthread_create error: error_code=" << ret << endl;
        }
    }
    //等各个线程退出后,进程才结束,否则进程强制结束了,线程可能还没反应过来;
    pthread_exit(NULL);
}

使用 -lpthread 库编译下面的程序:(直接在vscode里编译会失败,使用windows powershell或者linux的shell来执行)

PS D:\MyC++> g++ test.cpp -lpthread -o test.exe

现在,执行程序,将产生下列结果:

PS D:\MyC++> .\test.exe
Hello boy!
Hello boy!
Hello boy!
Hello boy!
Hello boy!

PS D:\MyC++>

以下简单的实例代码使用 pthread_create() 函数创建了 5 个线程,并接收传入的参数。每个线程打印一个 "Hello boy!" 消息,并输出接收的参数,然后调用 pthread_exit() 终止线程。

#include <iostream>
#include <cstdlib>
#include <pthread.h>
#include <windows.h> //包含头文件,解决中文乱码的依赖

using namespace std;

#define NUM_THREADS     5

void* PrintHello(void *threadid)
{  
   // 对传入的参数进行强制类型转换,由无类型指针变为整形数指针,然后再读取
   int tid = *((int*)threadid);
   cout << "Hello world!线程 ID: " << tid << endl;
   pthread_exit(NULL);
   return nullptr;  // 返回一个空指针
}

int main ()
{
   SetConsoleOutputCP(65001);  //***需要include <windows.h>
   pthread_t threads[NUM_THREADS];
   int indexes[NUM_THREADS];// 用数组来保存i的值
   int rc;
   int i;
   for( i=0; i < NUM_THREADS; i++ ){      
      cout << "main() : 创建线程, " << i << endl;
      indexes[i] = i; //先保存i的值
      // 传入的时候必须强制转换为void* 类型,即无类型指针        
      rc = pthread_create(&threads[i], NULL, 
                          PrintHello, (void *)&(indexes[i]));
      if (rc){
         cout << "Error:无法创建线程," << rc << endl;
         exit(-1);
      }
   }
   pthread_exit(NULL);
}

现在编译并执行程序,将产生下列结果:

PS D:\MyC++> g++ test.cpp -lpthread -o test.exe
PS D:\MyC++> .\test.exe
main() : 创建线程, 0
main() : 创建线程, 1
Hello world!线程 ID: 0
main() : 创建线程, 2
Hello world!线程 ID: 1
main() : 创建线程, 3
Hello world!线程 ID: 2
main() : 创建线程, 4
Hello world!线程 ID: 3
Hello world!线程 ID: 4
PS D:\MyC++>

向线程传递参数

这个实例演示了如何通过结构体传递多个参数。您可以在线程回调中传递任意的数据类型,因为它指向 void,如下面的实例所示:

#include <iostream>
#include <cstdlib>
#include <pthread.h>

using namespace std;

#define NUM_THREADS     5

struct thread_data{
   int  thread_id;
   char *message;
};

void *PrintHello(void *threadarg)
{
   struct thread_data *my_data;

   my_data = (struct thread_data *) threadarg;

   cout << "Thread ID : " << my_data->thread_id ;
   cout << " Message : " << my_data->message << endl;

   return pthread_exit(NULL);
}

int main ()
{
   pthread_t threads[NUM_THREADS];
   struct thread_data td[NUM_THREADS];
   int rc;
   int i;

   for( i=0; i < NUM_THREADS; i++ ){
      cout <<"main() : creating thread, " << i << endl;
      td[i].thread_id = i;
      td[i].message = "This is message";
      rc = pthread_create(&threads[i], NULL,
                          PrintHello, (void *)&td[i]);
      if (rc){
         cout << "Error:unable to create thread," << rc << endl;
         exit(-1);
      }
   }
   pthread_exit(NULL);
}

当上面的代码被编译和执行时,它会产生下列结果:

$ g++ -Wno-write-strings test.cpp -lpthread -o test.o
$ ./test.o
main() : creating thread, 0
main() : creating thread, 1
main() : creating thread, 2
main() : creating thread, 3
main() : creating thread, 4
Thread ID : 3 Message : This is message
Thread ID : 2 Message : This is message
Thread ID : 0 Message : This is message
Thread ID : 1 Message : This is message
Thread ID : 4 Message : This is message

thread库

注意:

如果使用的时minGW作为开发环境,那么在使用thread时可能会遇到比那一问题:

"namespace "std" has no member "thread""

解决办法是:

重装minGW;在重装时,把Threads选择为:posix。


在C++中,可以使用多线程来实现并行执行任务的功能。C++标准库提供了一个线程支持库 std::thread,可以用于创建和管理线程。

下面是一个简单的示例,演示如何在C++中创建和运行多个线程:

#include <iostream>
#include <thread>

// 线程函数,打印一条消息
void threadFunction() {
    std::cout << "Hello from thread!" << std::endl;
}

int main() {
    // 创建一个新线程,并指定线程函数
    std::thread myThread(threadFunction);

    // 主线程继续执行其他任务
    std::cout << "Hello from main thread!" << std::endl;

    // 等待新线程执行完毕
    myThread.join();

    return 0;
}

在上述示例中,通过创建一个新的线程对象 myThread,将其关联到线程函数 threadFunction。主线程会继续执行后续任务,并打印出 "Hello from main thread!" 的消息。通过 myThread.join(),主线程会等待新线程执行完毕。

通过使用多线程,可以实现更高效的并行处理,提高程序的性能。需要注意的是,在多线程编程中,需要处理好线程之间的同步和互斥问题,确保数据的正确性和线程的安全性。

除了std::thread,C++标准库还提供了其他与多线程相关的类和函数,如互斥量 std::mutex、条件变量 std::condition_variable 等,用于实现线程的同步和通信。

C++中解决多线程的其他常见方法

C++中使用最多的多线程方法是通过标准库中的 <thread> 头文件提供的功能。以下是几种常用的多线程方法:

  1. std::thread类: 使用 std::thread 类可以创建和管理线程。您可以通过传递一个函数或可调用对象来创建线程,并使用 join()detach() 等方法来等待线程完成或分离线程。

示例代码:

#include <iostream>
#include <thread>

// 线程函数
void threadFunc() {
    // 线程执行的代码
    std::cout << "Hello from thread!" << std::endl;
}

int main() {
    // 创建线程并启动
    std::thread myThread(threadFunc);

    // 等待线程完成
    myThread.join();

    return 0;
}
  1. std::async函数: 使用 std::async 函数可以异步地执行函数,并返回一个与执行结果相关联的 std::future 对象,可以通过该对象获取函数的返回值。

示例代码:

#include <iostream>
#include <future>

// 异步执行的函数
int asyncFunc() {
    // 执行一些操作
    return 42;
}

int main() {
    // 启动异步任务
    std::future<int> result = std::async(asyncFunc);

    // 在需要时获取异步任务的结果
    int value = result.get();

    std::cout << "Result: " << value << std::endl;

    return 0;
}
  1. 互斥锁(mutex): 在多线程编程中,为了避免多个线程同时对共享数据进行访问而引发问题,可以使用互斥锁来实现线程之间的同步。通过 std::mutex 类定义和管理互斥锁,确保在任意时刻只有一个线程访问临界区。

示例代码:

#include <iostream>
#include <thread>
#include <mutex>

// 共享数据
int counter = 0;
std::mutex mutex;

// 线程函数
void threadFunc() {
    for (int i = 0; i < 100000; ++i) {
        std::lock_guard<std::mutex> lock(mutex);
        ++counter;
    }
}

int main() {
    // 创建多个线程并启动
    std::thread thread1(threadFunc);
    std::thread thread2(threadFunc);

    // 等待线程完成
    thread1.join();
    thread2.join();

    std::cout << "Counter value: " << counter << std::endl;

    return 0;
}

除了上述方法外,还存在其他一些支持多线程编程的库和框架,如Boost.Thread、OpenMP等,可以根据具体需求选择合适的多线程方案。

浅拷贝

在C++中,浅拷贝(Shallow Copy)指的是对对象进行拷贝时,仅仅复制对象的成员变量的值,而不会复制成员变量所指向的动态内存数据。

当对象包含指向动态内存的指针成员时,执行默认的拷贝构造函数或赋值运算符时将会进行浅拷贝。这意味着原对象和新对象的指针成员将指向同一块动态内存空间,它们共享同一个资源,当其中一个对象释放了该资源,另一个对象仍然指向了已释放的内存,导致悬垂指针和使用错误的内存。

换句话说,浅拷贝只是创建了一个新对象,并将原对象中的数据成员值复制到新对象中,但它们共享相同的资源。

浅拷贝示例1:

#include <iostream>

class MyClass {
public:
    int* data;

    MyClass(int value) {
        data = new int(value);
    }

    // 默认的拷贝构造函数
    MyClass(const MyClass& other) {
        data = other.data;  // 浅拷贝,直接复制指针
    }

    // 默认的赋值运算符重载
    MyClass& operator=(const MyClass& other) {
        if (this != &other) {
            delete data;  // 首先释放原有资源
            data = other.data;  // 浅拷贝,直接复制指针
        }
        return *this;
    }

    ~MyClass() {
        delete data;
    }
};

int main() {
    MyClass obj1(5);
    MyClass obj2 = obj1;  // 使用拷贝构造函数进行浅拷贝

    std::cout << *obj1.data << std::endl;  // 输出: 5
    std::cout << *obj2.data << std::endl;  // 输出: 5

    *obj1.data = 10;
  
    std::cout << *obj1.data << std::endl;  // 输出: 10
    std::cout << *obj2.data << std::endl;  // 输出: 10,因为两个对象共享同一个指针

    return 0;
}

在上述示例中,我们定义了一个类MyClass,其中包含一个动态分配的整型指针data。默认的拷贝构造函数和赋值运算符进行了浅拷贝操作,仅简单地复制了指针而没有创建新的内存空间。因此,当改变其中一个对象的数据时,另一个对象也会受到影响,因为它们共享相同的资源。

需要注意的是,由于多个对象共享同一块内存,释放一个对象的资源可能导致其他对象引用的资源无效(悬挂指针)。这是浅拷贝的一个潜在问题,可以通过深拷贝来避免,即复制指针所指向的内容而不是指针本身。

浅拷贝示例2:

#include <iostream>

class MyClass {
public:
    MyClass(int size) {
        data_size = size;
        data = new int[size];
    }

    ~MyClass() {
        delete[] data;
    }

    // 默认的拷贝构造函数(浅拷贝)
    MyClass(const MyClass& other) {
        data_size = other.data_size;
        data = other.data;  // 执行浅拷贝,两个指针指向相同的动态内存
    }

    void setData(int index, int value) {
        if (index >= 0 && index < data_size) {
            data[index] = value;
        }
    }

    void printData() {
        for (int i = 0; i < data_size; i++) {
            std::cout << data[i] << " ";
        }
        std::cout << std::endl;
    }

private:
    int* data;
    int data_size;
};

int main() {
    MyClass obj1(5);
    obj1.setData(0, 1);
    obj1.setData(1, 2);
    obj1.printData();  // 输出:1 2

    MyClass obj2(obj1);  // 执行浅拷贝,obj2和obj1共享同一块内存
    obj2.setData(0, 3);
    obj2.printData();  // 输出:3 2

    obj1.printData();  // 输出:3 2
    return 0;
}

深拷贝

在C++中,深拷贝(Deep Copy)是指对对象进行拷贝时,不仅复制对象的成员变量的值,还复制指向动态分配内存的指针或引用,创建一个独立的副本。换句话说,深拷贝会创建一个新对象,并完全复制原对象中的数据成员,包括动态分配的内存空间。

通过深拷贝,每个对象都有自己独立的资源,彼此之间没有共享关系。下面是一个示例:

#include <iostream>

class MyClass {
public:
    int* data;

    MyClass(int value) {
        data = new int(value);
    }

    // 深拷贝的拷贝构造函数
    MyClass(const MyClass& other) {
        data = new int(*other.data);  // 深拷贝,创建新的内存空间并复制值
    }

    // 深拷贝的赋值运算符重载
    MyClass& operator=(const MyClass& other) {
        if (this != &other) {
            delete data;  // 首先释放原有资源
            data = new int(*other.data);  // 深拷贝,创建新的内存空间并复制值
        }
        return *this;
    }

    ~MyClass() {
        delete data;
    }
};

int main() {
    MyClass obj1(5);
    MyClass obj2 = obj1;  // 使用拷贝构造函数进行深拷贝

    std::cout << *obj1.data << std::endl;  // 输出: 5
    std::cout << *obj2.data << std::endl;  // 输出: 5

    *obj1.data = 10;

    std::cout << *obj1.data << std::endl;  // 输出: 10
    std::cout << *obj2.data << std::endl;  // 输出: 5,因为两个对象有独立的指针和内存空间

    return 0;
}

在上述示例中,我们定义了一个类MyClass,其中包含一个动态分配的整型指针data。拷贝构造函数和赋值运算符实现了深拷贝操作,通过创建新的内存空间,将原对象的数据复制到新对象中。因此,每个对象都拥有独立的资源,对一个对象的修改不会影响其他对象。

需要注意的是,由于深拷贝涉及到内存的动态分配和释放,需要确保在适当的时候释放副本对象所拥有的资源,以避免内存泄漏。

深浅拷贝的综合示例

#include<iostream>
using namespace std;
class demo{
public:
    int* data;

    demo(int value){
        data = new int(value);
    }

    demo(demo& other){
        data = other.data;    //这个行为就是浅拷贝;没有为指针变量开辟专属的内存空间
    }

    demo(demo& other, int m){  //这里为了和demo(demo& other)区别,我们多加一个参数ssww
        data = new int(*other.data);   //这个行为就是深拷贝;为指针变量开辟专属的内存空间,值还是使用源对象的值*other.data
    }

    ~demo(){
        delete data;
    }
};

int main(){
    demo obj1(10);
    demo obj2(obj1);     //这个时候根据demo对象的构造函数定义会执行浅拷贝动作。
    cout << "查看obj1对象的data的地址:" << obj1.data << endl;
    cout << "查看obj1对象的data的值:" << *obj1.data << endl;     //10
    cout << "查看obj2对象的data的地址:" << obj2.data << endl;    //地址与obj1.data相同
    cout << "查看obj2对象的data的值:" << *obj2.data << endl;     //10

    //obj1.data = 20;   //这个写法错误;因为obj1.data实际上是一个指针;指针赋值要么将地址赋给它,要么解指针后进行赋值;
    *obj1.data = 20;
    /* 或者
    int x = 20;
    obj1.data = &x;
    */
    cout << "再次查看obj1对象的data的地址:" << obj1.data << endl;
    cout << "再次查看obj1对象的data的值:" << *obj1.data << endl;    //20
    cout << "再次查看obj2对象的data的地址:" << obj2.data << endl;   //地址与obj1.data相同
    cout << "再次查看obj2对象的data的值:" << *obj2.data << endl;    //20


    demo obj3(100);
    demo obj4(obj3, 888);    // 888随便写一个整数就行,我们的函数定义中根本没用到;仅仅为了避免重载。
    cout << "查看obj3对象的data的地址:" << obj3.data << endl;
    cout << "查看obj3对象的data的值:" << *obj3.data << endl;     //100
    cout << "查看obj4对象的data的地址:" << obj4.data << endl;    //地址与obj3.data不同
    cout << "查看obj4对象的data的值:" << *obj4.data << endl;     //100

    *obj3.data = 200;
    cout << "再次查看obj3对象的data的地址:" << obj3.data << endl;
    cout << "再次查看obj3对象的data的值:" << *obj3.data << endl;    //200
    cout << "再次查看obj4对象的data的地址:" << obj4.data << endl;   //地址与obj3.data不同
    cout << "再次查看obj4对象的data的值:" << *obj4.data << endl;    //100

    return 0;
}

输出结果:

查看obj1对象的data的地址:0x741920
查看obj1对象的data的值:10
查看obj2对象的data的地址:0x741920
查看obj2对象的data的值:10
再次查看obj1对象的data的地址:0x741920
再次查看obj1对象的data的值:20
再次查看obj2对象的data的地址:0x741920
再次查看obj2对象的data的值:20
查看obj3对象的data的地址:0x741960
查看obj3对象的data的值:100
查看obj4对象的data的地址:0x7419a0
查看obj4对象的data的值:100
再次查看obj3对象的data的地址:0x741960
再次查看obj3对象的data的值:200
再次查看obj4对象的data的地址:0x7419a0
再次查看obj4对象的data的值:100

C++ "is a"和"have a"的区别

在面向对象编程中,"is a"和"have a"是两种不同的关系,用于描述类与类之间的关联。

  1. "is a"关系:
    "is a"关系用来表示继承关系,即一个类是另一个类的一种特化或派生。在这种关系中,派生类可以继承基类的属性和方法,并且可以增加自己的特性。它反映了类之间的一种分类关系。
    例如,"狗是动物"就可以用继承来表达,其中"动物"是基类,"狗"是派生类。派生类继承了基类的一些共性特征,如呼吸、移动等,同时还可以添加自己的特性,如吠声、尾巴摇动等。
class Animal {
public:
    void breathe() {
        // 实现呼吸功能
    }
    
    void move() {
        // 实现移动功能
    }
};

class Dog : public Animal {
public:
    void bark() {
        // 实现吠声功能
    }
};

int main() {
    Dog dog;
    dog.breathe();    // 调用继承自基类的函数
    dog.move();       // 调用继承自基类的函数
    dog.bark();       // 调用派生类自己的函数
    
    return 0;
}
  1. "has a"关系: “has a”关系表示一个类包含(拥有)另一个类的对象作为成员变量。这种关系通常通过组合(composition)实现,在一个类中可以创建其他类的对象,通过这些成员对象来实现功能。

示例代码如下:

class Engine {
public:
    void start() { cout << "Engine is starting" << endl; }
};

class Car {
private:
    Engine engine;

public:
    void startCar() { engine.start(); }
};

int main() {
    Car car;
    car.startCar();    // 输出:Engine is starting
    
    return 0;
}

在上面的例子中,Car类包含一个Engine对象作为成员变量,因此我们可以说“Car has an Engine”(汽车有一个引擎)。

总结: 在C++编程中,“is a”关系表示继承关系,子类是父类的一种类型或子类;而“has a”关系表示组合关系,一个类拥有另一个类的对象作为成员变量。


【C++程序设计】知识点汇总(常用知识点基本都有)_C++

本人水平有限,文章中难免有误,真诚欢迎大家斧正~

喜欢本文的朋友请三连哦!!!

另外本文也参考了网络上其他优秀博主的观点和实例,这里虽不能一一列举但内心属实感谢无私分享知识的每一位分享者。

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
  pHi3xXObtd3a   2023年11月02日   71   0   0 void*voidC++指针c
  ZPjjn0e4NYwF   2023年11月13日   34   0   0 C++
W7xauqsNtuHG