C++ 学习笔记1

C++

C++ 是一种静态类型的、编译式的、通用的、大小写敏感的、不规则的编程语言,支持过程化编程、面向对象编程和泛型编程。

C++ 是 C 的一个超集,事实上,任何合法的 C 程序都是合法的 C++ 程序。

注意:使用静态类型的编程语言是在编译时执行类型检查,而不是在运行时执行类型检查。

C++ 完全支持面向对象的程序设计,包括面向对象开发的四大特性:

  • 封装(Encapsulation):封装是将数据和方法组合在一起,对外部隐藏实现细节,只公开对外提供的接口。这样可以提高安全性、可靠性和灵活性。
  • 继承(Inheritance):继承是从已有类中派生出新类,新类具有已有类的属性和方法,并且可以扩展或修改这些属性和方法。这样可以提高代码的复用性和可扩展性。
  • 多态(Polymorphism):多态是指同一种操作作用于不同的对象,可以有不同的解释和实现。它可以通过接口或继承实现,可以提高代码的灵活性和可读性。
  • 抽象(Abstraction):抽象是从具体的实例中提取共同的特征,形成抽象类或接口,以便于代码的复用和扩展。抽象类和接口可以让程序员专注于高层次的设计和业务逻辑,而不必关注底层的实现细节。

C++ 关键字

c++ 关键字完整介绍

C++ 数据类型与变量类型

数据类型

类型 关键字
布尔型 bool
字符型 char
整型 int
浮点型 float
双浮点型 double
无类型 void
宽字符型 wchar_t

wchar_t 实际为:typedef short int wchar_t; ,与 short int 相同。

各数据类型大小及取值范围
类型 范围
char 1 个字节 -128 到 127 或者 0 到 255
unsigned char 1 个字节 0 到 255
signed char 1 个字节 -128 到 127
int 4 个字节 -2147483648 到 2147483647
unsigned int 4 个字节 0 到 4294967295
signed int 4 个字节 -2147483648 到 2147483647
short int 2 个字节 -32768 到 32767
unsigned short int 2 个字节 0 到 65,535
signed short int 2 个字节 -32768 到 32767
long int 8 个字节 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807
signed long int 8 个字节 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807
unsigned long int 8 个字节 0 到 18,446,744,073,709,551,615
float 4 个字节 精度型占4个字节(32位)内存空间,+/- 3.4e +/- 38 (~7 个数字)
double 8 个字节 双精度型占8 个字节(64位)内存空间,+/- 1.7e +/- 308 (~15 个数字)
long long 8 个字节 双精度型占8 个字节(64位)内存空间,表示 -9,223,372,036,854,775,807 到 9,223,372,036,854,775,807 的范围
long double 16 个字节 长双精度型 16 个字节(128位)内存空间,可提供18-19位有效数字。
wchar_t 2 或 4 个字节 1 个宽字符

不同位数操作系统的类型大小区别

piaHY6S.png

其它关于类型的知识点
  • typedef 声明
  • 枚举变量

类型转换

C++ 中有四种类型转换:静态转换、动态转换、常量转换和重新解释转换。

  1. 静态转换(Static Cast)

    静态转换是将一种数据类型的值强制转换为另一种数据类型的值。

    静态转换通常用于比较类型相似的对象之间的转换,例如将 int 类型转换为 float 类型。

    静态转换不进行任何运行时类型检查,因此可能会导致运行时错误。

    1
    2
    int i = 10;
    float f = static_cast<float>(i); // 静态将int类型转换为float类型
  2. 动态转换(Dynamic Cast)

    动态转换通常用于将一个基类指针或引用转换为派生类指针或引用。动态转换在运行时进行类型检查,如果不能进行转换则返回空指针或引发异常。

    1
    2
    3
    4
    class Base {};
    class Derived : public Base {};
    Base* ptr_base = new Derived;
    Derived* ptr_derived = dynamic_cast<Derived*>(ptr_base); // 将基类指针转换为派生类指针
  3. 常量转换(Const Cast)

    常量转换用于将 const 类型的对象转换为非 const 类型的对象。

    常量转换只能用于转换掉 const 属性,不能改变对象的类型。

    1
    2
    const int i = 10;
    int& r = const_cast<int&>(i); // 常量转换,将const int转换为int
  4. 重新解释转换(Reinterpret Cast)

    重新解释转换将一个数据类型的值重新解释为另一个数据类型的值,通常用于在不同的数据类型之间进行转换。

    重新解释转换不进行任何类型检查,因此可能会导致未定义的行为。

    1
    2
    int i = 10;
    float f = reinterpret_cast<float&>(i); // 重新解释将int类型转换为float类型

变量类型

  1. 整数类型(Integer Types):
    • int:用于表示整数,通常占用4个字节。
    • short:用于表示短整数,通常占用2个字节。
    • long:用于表示长整数,通常占用4个字节。
    • long long:用于表示更长的整数,通常占用8个字节。
  2. 浮点类型(Floating-Point Types):
    • float:用于表示单精度浮点数,通常占用4个字节。
    • double:用于表示双精度浮点数,通常占用8个字节。
    • long double:用于表示更高精度的浮点数,占用字节数可以根据实现而变化。
  3. 字符类型(Character Types):
    • char:用于表示字符,通常占用1个字节。
    • wchar_t:用于表示宽字符,通常占用2或4个字节。
    • char16_t:用于表示16位Unicode字符,占用2个字节。
    • char32_t:用于表示32位Unicode字符,占用4个字节。
  4. 布尔类型(Boolean Type):
    • bool:用于表示布尔值,只能取truefalse
  5. 枚举类型(Enumeration Types):
    • enum:用于定义一组命名的整数常量。
  6. 指针类型(Pointer Types):
    • type*:用于表示指向类型为type的对象的指针。
  7. 数组类型(Array Types):
    • type[]type[size]:用于表示具有相同类型的元素组成的数组。
  8. 结构体类型(Structure Types):
    • struct:用于定义包含多个不同类型成员的结构。
  9. 类类型(Class Types):
    • class:用于定义具有属性和方法的自定义类型。
  10. 共用体类型(Union Types):
    • union:用于定义一种特殊的数据类型,它可以在相同的内存位置存储不同的数据类型。

extern 声明不会为变量开辟内存空间,也就是使用 extern 声明时不可以进行初始化。

变量作用域

定义变量的位置

  • 在函数或一个代码块内部声明的变量,称为局部变量
  • 在函数参数的定义中声明的变量,称为形式参数
  • 在所有函数外部声明的变量,称为全局变量

变量作用域种类

  • 局部作用域:在函数内部声明的变量具有局部作用域,它们只能在函数内部访问。局部变量在函数每次被调用时被创建,在函数执行完后被销毁。

  • 全局作用域:在所有函数和代码块之外声明的变量具有全局作用域,它们可以被程序中的任何函数访问。全局变量在程序开始时被创建,在程序结束时被销毁。

  • 块作用域:在代码块内部声明的变量具有块作用域,它们只能在代码块内部访问。块作用域变量在代码块每次被执行时被创建,在代码块执行完后被销毁。

  • 类作用域:在类内部声明的变量具有类作用域,它们可以被类的所有成员函数访问。类作用域变量的生命周期与类的生命周期相同。

变量初始化

在创建一个变量时,最好同时进行初始化。

局部变量不会被初始化,如果只声明了,没有初始化,则是一个未定义的随机值。

全局变量和 static 变量会被自动初始化为下列值:

数据类型 初始化默认值
int 0
char ‘\0’
float 0
double 0
pointer NULL

类作用域

类作用域指的是在类内部声明的变量,可以使用类名和作用域解析运算符 :: 来访问这个变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>

class MyClass {
public:
    static int class_var;  // 类作用域变量
};

int MyClass::class_var = 30;

int main() {
    std::cout << "类变量: " << MyClass::class_var << std::endl;
    return 0;
}

常量

字面量、const 修饰、#define 宏定义

整数常量

前缀指定基数:0x 或 0X 表示十六进制,0 表示八进制,不带前缀则默认表示十进制。

整数常量也可以带一个后缀,后缀是 U 和 L 的组合,U 表示无符号整数(unsigned),L 表示长整数(long)。后缀可以是大写,也可以是小写,U 和 L 的顺序任意。

浮点常量

浮点常量由整数部分、小数点、小数部分和指数部分组成。您可以使用小数形式或者指数形式来表示浮点常量。

当使用小数形式表示时,必须包含整数部分、小数部分,或同时包含两者。当使用指数形式表示时, 必须包含小数点、指数,或同时包含两者。带符号的指数是用 e 或 E 引入的。

科学技术法:314159E-5L

布尔常量

true 为 真;false 为 假。

不应把 true 的值看成 1,把 false 的值看成 0。

字符常量

字符常量是括在单引号中。如果常量以 L(仅当大写时)开头,则表示它是一个宽字符常量(例如 L’x’),此时它必须存储在 wchar_t 类型的变量中。否则,它就是一个窄字符常量(例如 ‘x’),此时它可以存储在 char 类型的简单变量中。

字符常量可以是一个普通的字符(例如 ‘x’)、一个转义序列(例如 ‘\t’),或一个通用的字符(例如 ‘\u02C0’)。

常见转义字符:

转义序列 含义
\ \ 字符
' ‘ 字符
" “ 字符
? ? 字符
\a 警报铃声
\b 退格键
\f 换页符
\n 换行符
\r 回车
\t 水平制表符
\v 垂直制表符
\ooo 一到三位的八进制数
\xhh . . . 一个或多个数字的十六进制数

字符串常量

字符串字面值或常量是括在双引号 "" 中的。一个字符串包含类似于字符常量的字符:普通的字符、转义序列和通用的字符。

您可以使用 \ 做分隔符,把一个很长的字符串常量进行分行。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <string>
using namespace std;

int main() {
    string greeting = "hello, runoob";
    cout << greeting;
    cout << "\n";     // 换行符
    string greeting2 = "hello, \
                       runoob";
    cout << greeting2;
    return 0;
}

修饰符类型

数据类型修饰符:

  • signed:表示变量可以存储负数。对于整型变量来说,signed 可以省略,因为整型变量默认为有符号类型。
  • unsigned:表示变量不能存储负数。对于整型变量来说,unsigned 可以将变量范围扩大一倍。
  • short:表示变量的范围比 int 更小。short int 可以缩写为 short。
  • long:表示变量的范围比 int 更大。long int 可以缩写为 long。
  • long long:表示变量的范围比 long 更大。C++11 中新增的数据类型修饰符。
  • float:表示单精度浮点数。
  • double:表示双精度浮点数。
  • bool:表示布尔类型,只有 true 和 false 两个值。
  • char:表示字符类型。
  • wchar_t:表示宽字符类型,可以存储 Unicode 字符。

修饰符 signed、unsigned、long 和 short 可应用于整型,signedunsigned 可应用于字符型,long 可应用于双精度型。

这些修饰符也可以组合使用,修饰符 signedunsigned 也可以作为 longshort 修饰符的前缀。例如:unsigned long int

C++ 允许使用速记符号来声明无符号短整数无符号长整数。您可以不写 int,只写单词 unsigned、shortlongint 是隐含的。

类型限定符

限定符 含义
const const 定义常量,表示该变量的值不能被修改。
volatile 修饰符 volatile 告诉该变量的值可能会被程序以外的因素改变,如硬件或其他线程。。
restrict restrict 修饰的指针是唯一一种访问它所指向的对象的方式。只有 C99 增加了新的类型限定符 restrict。
mutable 表示类中的成员变量可以在 const 成员函数中被修改。
static 用于定义静态变量,表示该变量的作用域仅限于当前文件或当前函数内,不会被其他文件或函数访问。
register 用于定义寄存器变量,表示该变量被频繁使用,可以存储在CPU的寄存器中,以提高程序的运行效率。

存储类

存储类定义 C++ 程序中变量/函数的范围(可见性)和生命周期。这些说明符放置在它们所修饰的类型之前。

  • auto
  • register
  • static
  • extern
  • mutable
  • thread_local (C++11)

从 C++ 17 开始,auto 关键字不再是 C++ 存储类说明符,且 register 关键字被弃用。

auto 存储类

自 C++ 11 以来,auto 关键字用于两种情况:声明变量时根据初始化表达式自动推断该变量的类型、声明函数时函数返回值的占位符。c++17 删除用法:用于自动变量的声明。

register 存储类

register 存储类用于定义存储在寄存器中而不是 RAM 中的局部变量(取决于硬件和实现的限制,不一定)。这意味着变量的最大尺寸等于寄存器的大小,且不能对它应用一元的 ‘&’ 运算符(因为它没有内存位置)。

static 存储类

static 存储类指示编译器在程序的生命周期内保持局部变量的存在,而不需要在每次它进入和离开作用域时进行创建和销毁。

static 修饰符也可以应用于全局变量。当 static 修饰全局变量时,会使变量的作用域限制在声明它的文件内

  1. static 修饰类的成员变量
  • 静态成员变量是先于类的对象而存在
  • 这个类的所有对象共用一个静态成员
  • 如果静态成员是公有的,那么可以直接通过类名调用
  • 静态成员数据在类内声明,类外初始化
  1. static 修饰类的成员方法
  • 静态成员函数是先于类的对象而存在
  • 可用类名直接调用(公有)
  • 在静态成员函数中没有this指针,所以不能使用非静态成员

extern 存储类

extern 是用来在另一个文件中声明一个全局变量或函数。头文件中声明,源文件中定义。

extern 存储类用于提供一个全局变量的引用,全局变量对所有的程序文件都是可见的。当您使用 ‘extern’ 时,对于无法初始化的变量,会把变量名指向一个之前定义过的存储位置。

mutable 存储类

mutable 说明符仅适用于类的对象,它允许对象的成员替代常量。也就是说,类中的 const 成员函数,如果有被 mutable 修饰的成员变量,则该变量是可以被修改的

thread_local 存储类

使用 thread_local 说明符声明的变量仅可在它在其上创建的线程上访问。 变量在创建线程时创建,并在销毁线程时销毁。 每个线程都有其自己的变量副本。

thread_local 说明符可以与 staticextern 合并。

可以将 thread_local 仅应用于数据声明和定义,thread_local 不能用于函数声明或定义。

1
2
3
4
5
6
7
8
9
10
11
thread_local int x;  // 命名空间下的全局变量
class X
{
static thread_local std::string s; // 类的static成员变量
};
static thread_local std::string X::s; // X::s 是需要定义的

void foo()
{
thread_local std::vector<int> v; // 本地变量
}

类中 const 修饰成员变量不能赋初值

类内可以声明const变量时同时赋初值。这在C++11标准之前是不允许的,但是在C++11标准之后,可以在类内声明const变量并在声明时直接赋初值。

对于类中声明的 const 成员,需要在类的构造函数的成员初始化列表中进行初始化。这是因为 const 成员在创建对象时就必须被赋初值,而构造函数的成员初始化列表是在对象被创建时执行的。

1
2
3
4
5
6
7
8
class foo{
public:
foo():i(100){}
private:
const int i=100; // c11 之后可以
};
//或者通过这样的方式来进行初始化
foo::foo():i(100){}

运算符

算数运算符

运算符 描述 实例
+ 把两个操作数相加 A + B 将得到 30
- 从第一个操作数中减去第二个操作数 A - B 将得到 -10
* 把两个操作数相乘 A * B 将得到 200
/ 分子除以分母 B / A 将得到 2
% 取模运算符,整除后的余数 B % A 将得到 0
++ 自增运算符,整数值增加 1 A++ 将得到 11
自减运算符,整数值减少 1 A– 将得到 9

关系运算符

运算符 描述 实例
== 检查两个操作数的值是否相等,如果相等则条件为真。 (A == B) 不为真。
!= 检查两个操作数的值是否相等,如果不相等则条件为真。 (A != B) 为真。
> 检查左操作数的值是否大于右操作数的值,如果是则条件为真。 (A > B) 不为真。
< 检查左操作数的值是否小于右操作数的值,如果是则条件为真。 (A < B) 为真。
>= 检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。 (A >= B) 不为真。
<= 检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。 (A <= B) 为真。

逻辑运算符

运算符 描述 实例
&& 称为逻辑与运算符。如果两个操作数都 true,则条件为 true。 (A && B) 为 false。
|| 称为逻辑或运算符。如果两个操作数中有任意一个 true,则条件为 true。 (A || B) 为 true。
! 称为逻辑非运算符。用来逆转操作数的逻辑状态,如果条件为 true 则逻辑非运算符将使其为 false。 !(A && B) 为 true。

位运算符

运算符 描述 实例
& 按位与操作,按二进制位进行”与”运算。运算规则: (A & B) 将得到 12,即为 0000 1100
| 按位或运算符,按二进制位进行”或”运算。运算规则: (A | B)将得到 61,即为 0011 1101
^ 异或运算符,按二进制位进行”异或”运算。运算规则: (A ^ B) 将得到 49,即为 0011 0001
~ 取反运算符,按二进制位进行”取反”运算。运算规则: (~A ) 将得到 -61,即为 1100 0011,一个有符号二进制数的补码形式。
<< 二进制左移运算符。将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0) A << 2 将得到 240,即为 1111 0000
>> 二进制右移运算符。将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。 A >> 2 将得到 15,即为 0000 1111
原码、反码、补码

原码、反码、补码参考

~x = -(x+1)

如二进制的 1 = 0000 0001(最高位为符号位,0代表正数,1代表负数)

正数:原码 = 反码 = 补码

正数按位取反过程:

  1. 先对正数求补码(整数补码与原码相同)
  2. 然后对补码取反,包括符号位
  3. 最后进行一个补码求原码的过程(看符号位是0还是1,如果是0,负数的原码需要按负数求补码的逆运算。)

负数按位取反过程

  1. 对负数求补码
  2. 对补码取反,包括符号位
  3. 最后进行一个补码求原码的过程(看符号位是0还是1,如果是0,正数的原码与补码相同)

负数:

举例 -1

原码:1000 0001

反码:1111 1110 (原码的基础上,符号位不变,其余位取反)

补码:1111 1111 (原码的基础上, 符号位不变, 其余各位取反, 最后+1 (即在反码的基础上+1))

举例:

按位取反 1:即 ~1 = -2

原码:1 = 0000 0001

取反:~1 = 1111 1110 (取反后为补码,此时反求原码)

首先减1,得到 1111 1101,然后符号位不变,其余位取反得到 1000 0010,即结果为 -2。

举例:

按位取反 -1:即 ~-1 = 0

原码:1000 0001

反码:1111 1110

补码:1111 1111

最后将补码取反,得到结果 0000 0000,即 0。

赋值运算符

运算符 描述 实例
= 简单的赋值运算符,把右边操作数的值赋给左边操作数 C = A + B 将把 A + B 的值赋给 C
+= 加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数 C += A 相当于 C = C + A
-= 减且赋值运算符,把左边操作数减去右边操作数的结果赋值给左边操作数 C -= A 相当于 C = C - A
*= 乘且赋值运算符,把右边操作数乘以左边操作数的结果赋值给左边操作数 C *= A 相当于 C = C * A
/= 除且赋值运算符,把左边操作数除以右边操作数的结果赋值给左边操作数 C /= A 相当于 C = C / A
%= 求模且赋值运算符,求两个操作数的模赋值给左边操作数 C %= A 相当于 C = C % A
<<= 左移且赋值运算符 C <<= 2 等同于 C = C << 2
>>= 右移且赋值运算符 C >>= 2 等同于 C = C >> 2
&= 按位与且赋值运算符 C &= 2 等同于 C = C & 2
^= 按位异或且赋值运算符 C ^= 2 等同于 C = C ^ 2
|= 按位或且赋值运算符 C |= 2 等同于 C = C | 2

杂项运算符

运算符 描述
sizeof sizeof 运算符返回变量的大小。例如,sizeof(a) 将返回 4,其中 a 是整数。
Condition ? X : Y 条件运算符。如果 Condition 为真 ? 则值为 X : 否则值为 Y。
, 逗号运算符会顺序执行一系列运算。整个逗号表达式的值是以逗号分隔的列表中的最后一个表达式的值。
.(点)和 ->(箭头) 成员运算符用于引用类、结构和共用体的成员。
Cast 强制转换运算符把一种数据类型转换为另一种数据类型。例如,int(2.2000) 将返回 2。
& 指针运算符 & 返回变量的地址。例如 &a; 将给出变量的实际地址。
* 指针运算符 * 指向一个变量。例如,*var; 将指向变量 var。

运算符优先级

用小括号。

类别 运算符 结合性
后缀 () [] -> . ++ - - 从左到右
一元 + - ! ~ ++ - - (type)* & sizeof 从右到左
乘除 * / % 从左到右
加减 + - 从左到右
移位 << >> 从左到右
关系 < <= > >= 从左到右
相等 == != 从左到右
位与 AND & 从左到右
位异或 XOR ^ 从左到右
位或 OR | 从左到右
逻辑与 AND && 从左到右
逻辑或 OR || 从左到右
条件 ?: 从右到左
赋值 = += -= *= /= %=>>= <<= &= ^= |= 从右到左
逗号 , 从左到右

循环

循环类型

C++ 编程语言提供了以下几种循环类型。点击链接查看每个类型的细节。

循环类型 描述
while 循环 当给定条件为真时,重复语句或语句组。它会在执行循环主体之前测试条件。
for 循环 多次执行一个语句序列,简化管理循环变量的代码。
do…while 循环 除了它是在循环主体结尾测试条件外,其他与 while 语句类似。
嵌套循环 您可以在 while、for 或 do..while 循环内使用一个或多个循环。

循环控制语句

循环控制语句更改执行的正常序列。当执行离开一个范围时,所有在该范围中创建的自动对象都会被销毁。

C++ 提供了下列的控制语句。点击链接查看每个语句的细节。

控制语句 描述
break 语句 终止 loopswitch 语句,程序流将继续执行紧接着 loop 或 switch 的下一条语句。
continue 语句 引起循环跳过主体的剩余部分,立即重新开始测试条件。
goto 语句 将控制转移到被标记的语句。但是不建议在程序中使用 goto 语句。

判断

判断语句

C++ 编程语言提供了以下类型的判断语句。点击链接查看每个语句的细节。

语句 描述
if 语句 一个 if 语句 由一个布尔表达式后跟一个或多个语句组成。
if…else 语句 一个 if 语句 后可跟一个可选的 else 语句,else 语句在布尔表达式为假时执行。
嵌套 if 语句 您可以在一个 ifelse if 语句内使用另一个 ifelse if 语句。
switch 语句 一个 switch 语句允许测试一个变量等于多个值时的情况。
嵌套 switch 语句 您可以在一个 switch 语句内使用另一个 switch 语句。

? : 运算符

我们已经在前面的章节中讲解了 条件运算符 ? :,可以用来替代 if…else 语句。它的一般形式如下:

1
Exp1 ? Exp2 : Exp3;

函数

函数参数

如果函数要使用参数,则必须声明接受参数值的变量。这些变量称为函数的形式参数

形式参数就像函数内的其他局部变量,在进入函数时被创建,退出函数时被销毁。

当调用函数时,有三种向函数传递参数的方式:

调用类型 描述
传值调用 该方法把参数的实际值赋值给函数的形式参数。在这种情况下,修改函数内的形式参数对实际参数没有影响。
指针调用 该方法把参数的地址赋值给形式参数。在函数内,该地址用于访问调用中要用到的实际参数。这意味着,修改形式参数会影响实际参数。
引用调用 该方法把参数的引用赋值给形式参数。在函数内,该引用用于访问调用中要用到的实际参数。这意味着,修改形式参数会影响实际参数。

参数的默认值

当您定义一个函数,您可以为参数列表中后边的每一个参数指定默认值。当调用函数时,如果实际参数的值留空,则使用这个默认值。

这是通过在函数定义中使用赋值运算符来为参数赋值的。调用函数时,如果未传递参数的值,则会使用默认值,如果指定了值,则会忽略默认值,使用传递的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
using namespace std;

int sum(int a, int b=20) // 在第一个给了默认参数值后面的参数都要给默认参数值。
{
int result;

result = a + b;

return (result);
}

int main ()
{
// 局部变量声明
int a = 100;
int b = 200;
int result;

// 调用函数来添加值
result = sum(a, b);
cout << "Total value is :" << result << endl;

// 再次调用函数
result = sum(a);
cout << "Total value is :" << result << endl;

return 0;
}

lambda 函数

Lambda函数的语法定义如下:

1
[capture](parameters) mutable ->return-type{statement}
  • **[capture]**:捕捉列表。捕捉列表总是出现在 lambda 表达式的开始处。事实上,[] 是 lambda 引出符。编译器根据该引出符判断接下来的代码是否是 lambda 函数。捕捉列表能够捕捉上下文中的变量供 lambda 函数使用。

  • (parameters):参数列表。与普通函数的参数列表一致。如果不需要参数传递,则可以连同括号 () 一起省略。

  • mutable:mutable 修饰符。默认情况下,lambda 函数总是一个 const 函数,mutable 可以取消其常量性。在使用该修饰符时,参数列表不可省略(即使参数为空)。

捕获列表是值捕获时,默认不可以在lambda函数体内修改值。

但是使用 mutable 修饰后,则可以修改,但修改的结果仅限于函数体内,函数体外依然是原来的值,因为只是值捕获。

  • ->return_type:返回类型。用追踪返回类型形式声明函数的返回类型。出于方便,不需要返回值的时候也可以连同符号 -> 一起省略。此外,在返回类型明确的情况下,也可以省略该部分,让编译器对返回类型进行推导。

  • **{statement}**:函数体。内容与普通函数一样,不过除了可以使用参数之外,还可以使用所有捕获的变量。

捕获列表:

  • []:默认不捕获任何变量;

  • [=]:默认以值捕获所有变量;

  • [&]:默认以引用捕获所有变量;

  • [x]:仅以值捕获x,其它变量不捕获;

  • [&x]:仅以引用捕获x,其它变量不捕获;

  • [=, &x]:默认以值捕获所有变量,但是x是例外,通过引用捕获;

  • [&, x]:默认以引用捕获所有变量,但是x是例外,通过值捕获;

  • [this]:通过引用捕获当前对象(其实是复制指针);

    使用[this]表示Lambda表达式会以引用的方式捕获当前对象的this指针,即Lambda表达式可以访问当前对象的成员变量和成员函数。

  • [*this]:通过传值方式捕获当前对象;

    使用[*this]表示Lambda表达式会以复制的方式捕获当前对象的this指针,即Lambda表达式可以访问当前对象的成员变量和成员函数,并且对当前对象的成员变量进行修改不会影响原对象。

对于[=]或[&]的形式,lambda 表达式可以直接使用 this 指针。但是,对于[]的形式,如果要使用 this 指针,必须显式传入:

1
[this]() { this->someFunc(); }();

在 lambda 函数的定义式中,参数列表和返回类型都是可选部分,而捕捉列表和函数体都可能为空,C++ 中最简单的 lambda 函数只需要声明为:

1
[]{};

lambda 函数使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

using namespace std;

int main()
{
int a = 10;
cout << "first:" << a << endl; // 10

auto test = [a]() mutable -> int {
a = 20;
cout << "lambda:" << a << endl; // 20
return a;
};

int res = test();

cout << "last:" << a << endl; // 10
cout << "res:" << res << endl; // 20

return 0;
}

数字

数学运算

#include <cmath>

序号 函数 & 描述
1 double cos(double); 该函数返回弧度角(double 型)的余弦。
2 double sin(double); 该函数返回弧度角(double 型)的正弦。
3 double tan(double); 该函数返回弧度角(double 型)的正切。
4 double log(double); 该函数返回参数的自然对数。
5 double pow(double, double); 假设第一个参数为 x,第二个参数为 y,则该函数返回 x 的 y 次方。
6 double hypot(double, double); 该函数返回两个参数的平方总和的平方根,也就是说,参数为一个直角三角形的两个直角边,函数会返回斜边的长度。
7 double sqrt(double); 该函数返回参数的平方根。
8 int abs(int); 该函数返回整数的绝对值。
9 double fabs(double); 该函数返回任意一个浮点数的绝对值。
10 double floor(double); 该函数返回一个小于或等于传入参数的最大整数。

随机数

如要产生 [m,n] 范围内的随机数 num,可用:**int num=rand()%(n-m+1)+m;**。
rand() 和 srand() 生成的是伪随机数;真随机数可以使用#include <random>库。

伪随机数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <ctime>
#include <cstdlib> // 包含 rand 和 srand

using namespace std;

int main ()
{
int i,j;

// 设置种子
srand( (unsigned)time( NULL ) );

/* 生成 10 个随机数 */
for( i = 0; i < 10; i++ )
{
// 生成实际的随机数
j= rand();
cout <<"随机数: " << j << endl;
}

return 0;
}

真随机数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <random>
#include <iostream>

using namespace std;

int main()
{
random_device rd; // non-deterministic generator
mt19937 gen(rd()); // to seed mersenne twister.
uniform_int_distribution<> dist(1,6); // distribute results between 1 and 6 inclusive.

for (int i = 0; i < 5; ++i) {
cout << dist(gen) << " "; // pass the generator to the distribution.
}
cout << endl;
}

数组

声明数组、初始化数组、访问数组元素。

在 C++ 中,数组是非常重要的,我们需要了解更多有关数组的细节。下面列出了 C++ 程序员必须清楚的一些与数组相关的重要概念:

概念 描述
多维数组 C++ 支持多维数组。多维数组最简单的形式是二维数组。
指向数组的指针 您可以通过指定不带索引的数组名称来生成一个指向数组中第一个元素的指针。
传递数组给函数 您可以通过指定不带索引的数组名称来给函数传递一个指向数组的指针。
从函数返回数组 C++ 允许从函数返回数组。

setw() 函数

setw 函数属于 C++ 中 <iomanip> 头文件提供的一组用于格式化输出的工具函数。setw 主要用于设置字段宽度,即输出的宽度。它是通过设置流的标志(width)来实现的。

n 表示宽度,用数字表示。

setw() 函数只对紧接着的输出产生作用。

当后面紧跟着的输出字段长度小于 n 的时候,在该字段前面用空格补齐,当输出字段长度大于 n 时,全部整体输出。

pid7F6U.png

setw() 默认填充的内容为空格,可以 setfill() 配合使用设置其他字符填充。

1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <iomanip>

using namespace std;

int main()
{
    cout << setfill('*')  << setw(14) << "runoob" << endl; // ********runoob
    return 0;
}

字符串

c++ 中有两种字符串:

  • C 风格字符串
  • C++ 引入的 string 类类型

C风格字符串

字符串实际上是使用 null 字符 \0 终止的一维字符数组。因此,一个以 null 结尾的字符串,包含了组成字符串的字符。

1
2
char site[7] = {'R', 'U', 'N', 'O', 'O', 'B', '\0'};
char site[] = "RUNOOB";

想要使用 C 语言中的字符串函数,C++ 中需要包含头文件 #include <cstring> 。strcpy、strcat、strlen。

string 类类型

string类提供了一系列针对字符串的操作,比如:

  • append() – 在字符串的末尾添加字符
  • find() – 在字符串中查找字符串
  • insert() – 插入字符
  • length() – 返回字符串的长度
  • replace() – 替换字符串
  • substr() – 返回某个子字符串

C++ 中常见的几种输入字符串方法

参考

cin、cin.get()、cin.getline()、getline()、gets()、getchar()

原始字符串(c11)

R”(字符串)” 为默认格式的原始字符串,原始字符串内的字符不进行转义。

1
2
3
4
5
6
7
#include<iostream>
using namespace std;
int main ()
{
cout << R"(原始\t字\n符串)" << endl;
return 0;
}

指针

指针

引用

引用变量是一个别名,也就是说,它是某个已存在变量的另一个名字。一旦把引用初始化为某个变量,就可以使用该引用名称或变量名称来指向变量。

  • 不存在空引用。引用必须连接到一块合法的内存。
  • 一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象。
  • 引用必须在创建时被初始化。指针可以在任何时间被初始化。

引用通常用于函数参数列表和函数返回值。下面列出了 C++ 程序员必须清楚的两个与 C++ 引用相关的重要概念:

概念 描述
把引用作为参数 C++ 支持把引用作为参数传给函数,这比传一般的参数更安全。
把引用作为返回值 可以从 C++ 函数中返回引用,就像返回其他数据类型一样。

当返回一个引用时,要注意被引用的对象不能超出作用域。所以返回一个对局部变量的引用是不合法的,但是,可以返回一个对静态变量的引用。

下列示例的数组也是定义在全局区的,所以函数中返回这个数组的引用依旧有效。

把引用作为返回值示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>

using namespace std;

double vals[] = {10.1, 12.6, 33.1, 24.1, 50.0};

double& setValues(int i) {
double& ref = vals[i];
return ref; // 返回第 i 个元素的引用,ref 是一个引用变量,ref 引用 vals[i]
}

// 要调用上面定义函数的主函数
int main ()
{

cout << "改变前的值" << endl;
for ( int i = 0; i < 5; i++ )
{
cout << "vals[" << i << "] = ";
cout << vals[i] << endl;
}

setValues(1) = 20.23; // 改变第 2 个元素
setValues(3) = 70.8; // 改变第 4 个元素

cout << "改变后的值" << endl;
for ( int i = 0; i < 5; i++ )
{
cout << "vals[" << i << "] = ";
cout << vals[i] << endl;
}
return 0;
}

日期 & 时间

C++ 标准库没有提供所谓的日期类型。C++ 继承了 C 语言用于日期和时间操作的结构和函数。为了使用日期和时间相关的函数和结构,需要在 C++ 程序中引用 <ctime> 头文件。

有四个与时间相关的类型:clock_t、time_t、size_ttm。类型 clock_t、size_t 和 time_t 能够把系统时间和日期表示为某种整数。

结构类型 tm 把日期和时间以 C 结构的形式保存,tm 结构的定义如下:

1
2
3
4
5
6
7
8
9
10
11
struct tm {
int tm_sec; // 秒,正常范围从 0 到 59,但允许至 61
int tm_min; // 分,范围从 0 到 59
int tm_hour; // 小时,范围从 0 到 23
int tm_mday; // 一月中的第几天,范围从 1 到 31
int tm_mon; // 月,范围从 0 到 11
int tm_year; // 自 1900 年起的年数
int tm_wday; // 一周中的第几天,范围从 0 到 6,从星期日算起
int tm_yday; // 一年中的第几天,范围从 0 到 365,从 1 月 1 日算起
int tm_isdst; // 夏令时
};

下面是 C/C++ 中关于日期和时间的重要函数。

序号 函数 & 描述
1 time_t time(time_t *time); 该函数返回系统的当前日历时间,自 1970 年 1 月 1 日以来经过的秒数。如果系统没有时间,则返回 -1。
2 char *ctime(const time_t *time); 该返回一个表示当地时间的字符串指针,字符串形式 day month year hours:minutes:seconds year\n\0
3 struct tm *localtime(const time_t *time); 该函数返回一个指向表示本地时间的 tm 结构的指针。
4 clock_t clock(void); 该函数返回程序执行起(一般为程序的开头),处理器时钟所使用的时间。如果时间不可用,则返回 -1。
5 char * asctime ( const struct tm * time ); 该函数返回一个指向字符串的指针,字符串包含了 time 所指向结构中存储的信息,返回形式为:day month date hours:minutes:seconds year\n\0。
6 struct tm *gmtime(const time_t *time); 该函数返回一个指向 time 的指针,time 为 tm 结构,用协调世界时(UTC)也被称为格林尼治标准时间(GMT)表示。
7 time_t mktime(struct tm *time); 该函数返回日历时间,相当于 time 所指向结构中存储的时间。
8 double difftime ( time_t time2, time_t time1 ); 该函数返回 time1 和 time2 之间相差的秒数。
9 size_t strftime(); 该函数可用于格式化日期和时间为指定的格式。

当前日期和时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <ctime>

using namespace std;

int main( )
{
// 基于当前系统的当前日期/时间
time_t now = time(0);

// 把 now 转换为字符串形式
char* dt = ctime(&now);

cout << "本地日期和时间:" << dt << endl;

// 把 now 转换为 tm 结构
tm *gmtm = gmtime(&now);
dt = asctime(gmtm);
cout << "UTC 日期和时间:"<< dt << endl;
}

使用结构 tm 格式化时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <ctime>

using namespace std;

int main( )
{
// 基于当前系统的当前日期/时间
time_t now = time(0);

cout << "1970 到目前经过秒数:" << now << endl;

tm *ltm = localtime(&now);

// 输出 tm 结构的各个组成部分
cout << "年: "<< 1900 + ltm->tm_year << endl;
cout << "月: "<< 1 + ltm->tm_mon<< endl;
cout << "日: "<< ltm->tm_mday << endl;
cout << "时间: "<< ltm->tm_hour << ":";
cout << ltm->tm_min << ":";
cout << ltm->tm_sec << endl;
}

以标准化格式输出日期时间

2018-09-19 09:00:58

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <ctime>
#include <stdlib.h>
#include <stdio.h>

using namespace std;

string Get_Current_Date();

int main( )
{
// 将当前日期以 20** - ** - ** 格式输出
cout << Get_Current_Date().c_str() << endl;

getchar();
return 0;
}

string Get_Current_Date()
{
time_t nowtime;
nowtime = time(NULL); //获取日历时间
char tmp[64];
strftime(tmp,sizeof(tmp),"%Y-%m-%d %H:%M:%S",localtime(&nowtime));
return tmp;
}

基本的输入输出

C++ 的 I/O 发生在流中,流是字节序列。如果字节流是从设备(如键盘、磁盘驱动器、网络连接等)流向内存,这叫做输入操作。如果字节流是从内存流向设备(如显示屏、打印机、磁盘驱动器、网络连接等),这叫做输出操作

所以良好的编程实践告诉我们,使用 cerr 流来显示错误消息,而其他的日志消息则使用 clog 流来输出。

I/O 库头文件

头文件 函数和描述
该文件定义了 cin、cout、cerrclog 对象,分别对应于标准输入流、标准输出流、非缓冲标准错误流和缓冲标准错误流。
该文件通过所谓的参数化的流操纵器(比如 setwsetprecision),来声明对执行标准化 I/O 有用的服务。
该文件为用户控制的文件处理声明服务。我们将在文件和流的相关章节讨论它的细节。

标准输出流(cout)

预定义的对象 coutiostream 类的一个实例。cout 对象”连接”到标准输出设备,通常是显示屏。

标准输入流(cin)

预定义的对象 ciniostream 类的一个实例。cin 对象附属到标准输入设备,通常是键盘。

标准错误流(cerr)

预定义的对象 cerriostream 类的一个实例。cerr 对象附属到标准输出设备,通常也是显示屏,但是 cerr 对象是非缓冲的,且每个流插入到 cerr 都会立即输出。

标准日志流(clog)

预定义的对象 clogiostream 类的一个实例。clog 对象附属到标准输出设备,通常也是显示屏,但是 clog 对象是缓冲的。这意味着每个流插入到 clog 都会先存储在缓冲区,直到缓冲填满或者缓冲区刷新时才会输出。

输入输出流中的函数(模板):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <iomanip>
using namespace std;
int main()
{
cout<<setiosflags(ios::left|ios::showpoint); // 设左对齐,以一般实数方式显示
cout.precision(5); // 设置除小数点外有五位有效数字
cout<<123.456789<<endl;
cout.width(10); // 设置显示域宽10
cout.fill('*'); // 在显示区域空白处用*填充
cout<<resetiosflags(ios::left); // 清除状态左对齐
cout<<setiosflags(ios::right); // 设置右对齐
cout<<123.456789<<endl;
cout<<setiosflags(ios::left|ios::fixed); // 设左对齐,以固定小数位显示
cout.precision(3); // 设置实数显示三位小数
cout<<999.123456<<endl;
cout<<resetiosflags(ios::left|ios::fixed); //清除状态左对齐和定点格式
cout<<setiosflags(ios::left|ios::scientific); //设置左对齐,以科学技术法显示
cout.precision(3); //设置保留三位小数
cout<<123.45678<<endl;
return 0;
}

测试输出结果:

1
2
3
4
123.46
****123.46
999.123
1.235e+02

其中 cout.setf 跟 setiosflags 一样,cout.precision 跟 setprecision 一样,cout.unsetf 跟 resetiosflags 一样。

在C++中,cout.setf 是用于设置输出格式标志(output format flags)的成员函数。这个函数可以用来改变输出流的一些格式选项,比如控制输出的进制、设置浮点数的精度等。

以下是 cout.setf 的一般用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

int main() {
// 设置输出流的格式标志
std::cout.setf(std::ios::hex, std::ios::basefield); // 设置输出为十六进制

// 输出一个整数
std::cout << 255 << std::endl; // 输出:ff

// 恢复默认的输出格式
std::cout.unsetf(std::ios::hex);

// 输出一个整数,使用默认的十进制格式
std::cout << 255 << std::endl; // 输出:255

return 0;
}
1
2
3
4
5
6
7
8
9
setiosflags(ios::fixed) 固定的浮点显示 
setiosflags(ios::scientific) 指数表示
setiosflags(ios::left) 左对齐
setiosflags(ios::right) 右对齐
setiosflags(ios::skipws) 忽略前导空白
setiosflags(ios::uppercase) 16进制数大写输出
setiosflags(ios::lowercase) 16进制小写输出
setiosflags(ios::showpoint) 强制显示小数点
setiosflags(ios::showpos) 强制显示符号

cout.setf 常见的标志:

标志 功能
boolalpha 可以使用单词”true”和”false”进行输入/输出的布尔值.
oct 用八进制格式显示数值.
dec 用十进制格式显示数值.
hex 用十六进制格式显示数值.
left 输出调整为左对齐.
right 输出调整为右对齐.
scientific 用科学记数法显示浮点数.
fixed 用正常的记数方法显示浮点数(与科学计数法相对应).
showbase 输出时显示所有数值的基数.
showpoint 显示小数点和额外的零,即使不需要.
showpos 在非负数值前面显示”+(正号)”.
skipws 当从一个流进行读取时,跳过空白字符(spaces, tabs, newlines).
unitbuf 在每次插入以后,清空缓冲区.
internal 将填充字符回到符号和数值之间.
uppercase 以大写的形式显示科学记数法中的”e”和十六进制格式的”x”.

iostream 中定义的操作符:

操作符 描述 输入 输出
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 跳过所有前导空白字符

iomanip 中定义的操作符:

操作符 描述 输入 输出
resetiosflags(long f) 关闭被指定为f的标志
setbase(int base) 设置数值的基本数为base
setfill(int ch) 设置填充字符为ch
setiosflags(long f) 启用指定为f的标志
setprecision(int p) 设置数值的精度(四舍五入)
setw(int w) 设置域宽度为w

结构体

类与结构体在 C++ 中只有两点区别,除此这外无任何区别。

  • class 中默认的成员访问权限是 private 的,而 struct 中则是 public 的。

  • 从 class 继承默认是 private 继承,而从 struct 继承默认是 public 继承。

  • class 可以定义模板,而 struct 不可以。

  • C与C++结构体中,C 语言的结构体中不能有函数,C++ 语言的结构体中可以有函数,当然也可以有构造函数。

类和对象

类是 C++ 的核心特性,通常被称为用户定义的类型。

类用于指定对象的形式,是一种用户自定义的数据类型,它是一种封装了数据和函数的组合。类中的数据称为成员变量,函数称为成员函数。类可以被看作是一种模板,可以用来创建具有相同属性和行为的多个对象。

C++中,结构体(struct)和类(class)之间的主要区别在于默认的访问控制(成员的可见性)和继承的默认访问权限(结构体为 public,类为 private)。除了这些差异,结构体和类在语法上几乎是相同的,它们都可以包含成员变量、成员函数、构造函数等。

类定义

若是类中成员变量不赋值直接访问的话,值是未定义的,即取决于内存中的垃圾值。这通常是由于编译器分配的内存中的旧数据或无效数据。

1
2
3
4
5
6
7
class Box
{
public:
double length; // 盒子的长度
double breadth; // 盒子的宽度
double height; // 盒子的高度
};

定义类对象

1
2
Box Box1;          // 声明 Box1,类型为 Box
Box Box2; // 声明 Box2,类型为 Box

访问数据成员

.-> 来访问。

私有的成员和受保护的成员不能使用直接成员访问运算符 (.) 来直接访问。

类和对象的一些关键概念

概念 描述
类成员函数 类的成员函数是指那些把定义和原型写在类定义内部的函数,就像类定义中的其他变量一样。
类访问修饰符 类成员可以被定义为 public、private 或 protected。默认情况下是定义为 private。
构造函数 & 析构函数 类的构造函数是一种特殊的函数,在创建一个新的对象时调用。类的析构函数也是一种特殊的函数,在删除所创建的对象时调用。
C++ 拷贝构造函数 拷贝构造函数,是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。
C++ 友元函数 友元函数可以访问类的 private 和 protected 成员。
C++ 内联函数 通过内联函数,编译器试图在调用函数的地方扩展函数体中的代码。
C++ 中的 this 指针 每个对象都有一个特殊的指针 this,它指向对象本身。
C++ 中指向类的指针 指向类的指针方式如同指向结构的指针。实际上,类可以看成是一个带有函数的结构。
C++ 类的静态成员 类的数据成员和函数成员都可以被声明为静态的。
类成员函数

类的成员函数是指那些把定义和原型写在类定义内部的函数,就像类定义中的其他变量一样。类成员函数是类的一个成员,它可以操作类的任意对象,可以访问对象中的所有成员。

成员函数可以定义在类定义内部,或者单独使用范围解析运算符 :: 来定义。在类定义中定义的成员函数把函数声明为内联的,即便没有使用 inline 标识符。

  1. 写在类定义内部

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Box
    {
    public:
    double length; // 长度
    double breadth; // 宽度
    double height; // 高度

    double getVolume(void)
    {
    return length * breadth * height;
    }
    };
  2. 类外部使用范围解析运算符::

1
2
3
4
double Box::getVolume(void)
{
return length * breadth * height;
}

在 :: 运算符之前必须使用类名。调用成员函数是在对象上使用点运算符(.)。

:: 可以不跟类名,表示全局数据或全局函数(即非成员函数)。

如果在类中未给出成员函数定义,而又想内联该函数的话,那在类外要加上 inline,否则就认为不是内联的。

1
2
3
4
5
6
7
8
// 头文件
class A
{
public:
void Foo(int x, int y);
}
// 定义文件
inline void A::Foo(int x, int y){}
类访问修饰符

它防止函数直接访问类类型的内部成员。类成员的访问限制是通过在类主体内部对各个区域标记 public、private、protected 来指定的。

关键字 public、private、protected 称为访问修饰符。

成员和类的默认访问修饰符是 private。

  • 公有(public)成员
    公有成员在程序中类的外部是可访问的。

  • 私有(private)成员
    私有成员变量或函数在类的外部是不可访问的,甚至是不可查看的。只有类和友元函数可以访问私有成员。默认情况下,类的所有成员都是私有的。

    一般会在私有区域定义数据,在公有区域定义相关的函数,以便在类的外部也可以调用这些函数,即 get 和 set 方法。

  • protected(受保护)成员
    protected(受保护)成员变量或函数与私有成员十分相似,但有一点不同,protected(受保护)成员在派生类(即子类)中是可访问的。

继承中的特点

有public, protected, private三种继承方式,它们相应地改变了基类成员的访问属性。

  • public 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:public, protected, private
  • protected 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:protected, protected, private
  • private 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:private, private, private

但无论哪种继承方式,下面两点都没有改变:

  • private 成员只能被本类成员(类内)和友元访问,不能被派生类访问;
  • protected 成员可以被派生类访问。

如果继承时不显示声明是 private,protected,public 继承,则类默认是 private 继承,在 struct 中默认 public 继承

继承方式 基类的public成员 基类的protected成员 基类的private成员 继承引起的访问控制关系变化概括
public继承 仍为public成员 仍为protected成员 不可见 基类的非私有成员在子类的访问属性不变
protected继承 变为protected成员 变为protected成员 不可见 基类的非私有成员都为子类的保护成员
private继承 变为private成员 变为private成员 不可见 基类中的非私有成员都称为子类的私有成员

结构体继承的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>

using namespace std;

struct Test1
{
Test1(double para); // 与类除了默认继承权限问题,并无其它区别。
double num;
void print();
};

Test1::Test1(double para)
{
num = para;
}

void Test1::print()
{
cout << "struct print()" << endl;
}

struct Test2: public Test1
{
Test2(double para) : Test1(para){} // 为子类定义构造函数,并传入父类。
};

int main()
{
Test1 t(0.5);
Test2 t2(5.4);

cout << t.num << endl;
t2.print();


return 0;
}
类构造函数 & 析构函数

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

构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。

c++11 之前,类内变量只能在构造函数初始化列表中初始化,c++11 之后才可以在类内变量声明时进行初始化。

带参数的构造函数(有参构造)

默认的构造函数没有任何参数,但如果需要,构造函数也可以带有参数。这样在创建对象时就会给对象赋初始值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Line
{
public:
void setLength( double len );
double getLength( void );
Line(double len); // 这是构造函数

private:
double length;
};

// 成员函数定义,包括构造函数
Line::Line( double len)
{
cout << "Object is being created, length = " << len << endl;
length = len;
}

使用初始化列表来初始化字段

下列例子中,length 为类中定义的成员变量。

初始化顺序最好要按照变量在类声明的顺序一致,否则会导致先赋值后面的变量时,再赋值前面的变量,则导致前面的变量不被初始化。

1
2
3
4
Line::Line( double len, int num): length(len), size(num) // 类中实际先声明了length,然后声明了size。
{
cout << "Object is being created, length = " << len << endl;
}

类的析构函数

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

析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。

拷贝构造函数

拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。拷贝构造函数通常用于:

  • 通过使用另一个同类型的对象来初始化新创建的对象。
  • 复制对象把它作为参数传递给函数。
  • 复制对象,并从函数返回这个对象。

如果在类中没有定义拷贝构造函数,编译器会自行定义一个(浅拷贝)。

需要拷贝构造函数(深拷贝)的情况:

需要深拷贝构造函数是避免使用浅拷贝只将指针的值复制,导致释放时释放了两次。

当成员变量中有动态内存分配,在析构函数中需要进行手动释放。

  • 类带有指针变量,并有动态内存分配
  • 有成员表示在构造函数中分配的其他资源(也就是说在构造函数中使用 new 运算符为对象的成员变量分配动态内存)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <iostream>

using namespace std;

class Line
{
public:
int getLength( void );
Line( int len ); // 简单的构造函数
Line( const Line &obj); // 拷贝构造函数
~Line(); // 析构函数

private:
int *ptr;
};

// 成员函数定义,包括构造函数
Line::Line(int len)
{
cout << "调用构造函数" << endl;
// 为指针分配内存
ptr = new int;
*ptr = len;
}

Line::Line(const Line &obj)
{
cout << "调用拷贝构造函数并为指针 ptr 分配内存" << endl;
ptr = new int;
*ptr = *obj.ptr; // 拷贝值
}

Line::~Line(void)
{
cout << "释放内存" << endl;
delete ptr;
}
int Line::getLength( void )
{
return *ptr;
}

void display(Line obj)
{
cout << "line 大小 : " << obj.getLength() <<endl;
}

// 程序的主函数
int main( )
{
Line line1(10);

Line line2 = line1; // 这里也调用了拷贝构造函数

display(line1);
display(line2);

return 0;
}
友元(函数 & 类)

类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是某个类中的成员函数,因此使用友元函数时不需要作用域符,而是直接调用

友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。

  • 声明全局函数为一个类的友元,需要在类定义中该函数原型前使用关键字 friend

  • 声明为一个类的友元,则需要在类定义中放置 friend class 类名 的声明。

  • 声明友元成员函数

注意:

  • 友元关系不能被继承
  • 友元关系是单向的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>

using namespace std;

class Box
{
friend class ClassTwo; // 这里是一个友元类。
double width;
public:
friend void printWidth( Box box );
void setWidth( double wid );
};

// 成员函数定义
void Box::setWidth( double wid )
{
width = wid;
}

// 请注意:printWidth() 不是任何类的成员函数
void printWidth( Box box )
{
/* 因为 printWidth() 是 Box 的友元,它可以直接访问该类的任何成员 */
cout << "Width of box : " << box.width <<endl;
}

// 程序的主函数
int main( )
{
Box box;

// 使用成员函数设置宽度
box.setWidth(10.0);

// 使用友元函数输出宽度
printWidth( box );

return 0;
}

友元成员函数的声明

避免直接声明友元类导致被暴露的范围太大。

要把男朋友类CBoy的某成员函数声明为超女类CGirl的友元,声明和定义的顺序如下:

  1. 要被访问私有变量的类的前置声明。
  2. 需要访问私有变量的友元类的声明。
  3. 要被访问私有变量的类的声明、定义。
  4. 需要访问私有变量的友元类的成员函数定义。
1
2
3
4
5
6
class CGirl;      // 前置声明。  
class CBoy { ...... }; // CBoy的定义。
class CGirl { ...... }; // CGirl的定义。

// 友元成员函数的定义。
void CBoy::func(CGirl &g) { ...... }

完整声明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>         // 包含头文件。
using namespace std; // 指定缺省的命名空间。

class CGirl; // 把超女类的声明前置

class CBoy // 超女的男朋友类
{
public:
void func1(const CGirl& g);
void func2(const CGirl& g);
};

class CGirl // 超女类CGirl。
{
friend void CBoy::func1(const CGirl& g);
// friend void CBoy::func2(const CGirl& g);
public:
string m_name; // 姓名。
// 默认构造函数。
CGirl() { m_name = "西施"; m_xw = 87; }
// 显示姓名的成员函数。
void showname() { cout << "姓名:" << m_name << endl; }
private:
int m_xw; // 胸围。
// 显示胸围的成员函数。
void showxw() const { cout << "胸围:" << m_xw << endl; }
};

void CBoy::func1(const CGirl& g) { cout << "func1()我女朋友的胸围是:" << g.m_xw << endl; }
void CBoy::func2(const CGirl& g) { cout << "func2()我女朋友的姓名是:" << g.m_name << endl; }

int main()
{
CGirl g;
CBoy b;
b.func2(g);
b.func1(g);
}

友元函数没有 this 指针,使用参数的情况

可以直接调用友元函数,不需要通过对象或指针。

  • 要访问非static成员时,需要对象做参数;

  • 要访问static成员或全局变量时,则不需要对象做参数;

  • 如果做参数的对象是全局对象,则不需要对象做参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class INTEGER
{
friend void Print(const INTEGER& obj);//声明友元函数
};

void Print(const INTEGER& obj)
{
//函数体
}

void main()
{
INTEGER obj;
Print(obj);//直接调用
}
内联函数(inline)
  1. 若是成员函数在类中直接进行定义,则默认是内联函数。
  2. 若类中成员函数未给出定义,又想要是内联函数,则需要在类外显式定义 inline 函数,如 inline void ClassName::print(){}

详见前面的类定义章节。

this 指针

this 是指向自身实例的指针,即指向自身地址。

this 指针是一个特殊的指针,它指向当前对象的实例。每一个类的实例对象都能通过 this 指针来访问自己的地址。

this是一个隐藏的指针,可以在类的成员函数中使用,它可以用来指向调用对象。

当一个对象的成员函数被调用时,编译器会隐式地传递该对象的地址作为 this 指针,即当类中有一个 isbn 成员函数,在 isbn 成员函数被实例对象 total 调用时,实际过程为:Sales_data::isbn(&total) ,也就是将实例对象 total 的地址传入,也即是 this。

友元函数没有 this 指针,因为友元不是类的成员,只有成员函数才有 this 指针。

通过使用 this 指针,我们可以在成员函数中访问当前对象的成员变量,即使它们与函数参数或局部变量同名,这样可以避免命名冲突,并确保我们访问的是正确的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>

class MyClass {
private:
int value;

public:
void setValue(int value) {
this->value = value; // this指向的是类的成员变量,右侧value是指形参。
}

void printValue() {
std::cout << "Value: " << this->value << std::endl;
}
};

int main() {
MyClass obj;
obj.setValue(42);
obj.printValue();

return 0;
}
指向类的指针

一个指向 C++ 类的指针与指向结构的指针类似,访问指向类的指针的成员,需要使用成员访问运算符 ->,就像访问指向结构的指针一样。与所有的指针一样,您必须在使用指针之前,对指针进行初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <iostream>

using namespace std;

class Box
{
public:
// 构造函数定义
Box(double l=2.0, double b=2.0, double h=2.0)
{
cout <<"Constructor called." << endl;
length = l;
breadth = b;
height = h;
}
double Volume()
{
return length * breadth * height;
}
private:
double length; // Length of a box
double breadth; // Breadth of a box
double height; // Height of a box
};

int main(void)
{
Box Box1(3.3, 1.2, 1.5); // Declare box1
Box Box2(8.5, 6.0, 2.0); // Declare box2
Box *ptrBox; // Declare pointer to a class.

// 保存第一个对象的地址
ptrBox = &Box1;

// 现在尝试使用成员访问运算符来访问成员
cout << "Volume of Box1: " << ptrBox->Volume() << endl;

// 保存第二个对象的地址
ptrBox = &Box2;

// 现在尝试使用成员访问运算符来访问成员
cout << "Volume of Box2: " << ptrBox->Volume() << endl;

return 0;
}

类的静态成员

使用 static 关键字来把类成员定义为静态的。当我们声明类的成员为静态时,这意味着无论创建多少个类的对象,静态成员都只有一个副本,即静态成员在类的所有对象中是共享的。

piwvOrq.png

如果不存在其他的初始化语句,在创建第一个对象时,所有的静态数据都会被初始化为零。

我们不能把静态成员的初始化放置在类的定义中,但是可以在类的外部通过使用范围解析运算符 :: 来重新声明静态变量从而对它进行初始化。

静态成员变量在类中仅仅是声明,没有定义,所以要在类的外面定义,实际上是给静态成员变量分配内存。如果不加定义就会报错,初始化是赋一个初始值,而定义是分配内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>

using namespace std;

class Box
{
public:
static int objectCount;
// 构造函数定义
Box(double l=2.0, double b=2.0, double h=2.0)
{
cout <<"Constructor called." << endl;
length = l;
breadth = b;
height = h;
// 每次创建对象时增加 1
objectCount++;
}
double Volume()
{
return length * breadth * height;
}
private:
double length; // 长度
double breadth; // 宽度
double height; // 高度
};

// 初始化类 Box 的静态成员 其实是定义并初始化的过程
int Box::objectCount = 0;
//也可这样 定义却不初始化
//int Box::objectCount;

int main(void)
{
Box Box1(3.3, 1.2, 1.5); // 声明 box1
Box Box2(8.5, 6.0, 2.0); // 声明 box2

// 输出对象的总数
cout << "Total objects: " << Box::objectCount << endl;

return 0;
}

静态成员函数

如果把函数成员声明为静态的,就可以把函数与类的任何特定对象独立开来。静态成员函数即使在类对象不存在的情况下也能被调用,静态函数只要使用类名加范围解析运算符 :: 就可以访问。

静态成员函数只能访问静态成员数据、其他静态成员函数和类外部的其他函数。

静态成员函数有一个类范围,他们不能访问类的 this 指针。

类中的静态成员函数可以在类的定义内进行定义,而类中的静态成员变量是必须在类外定义的。

使用静态成员函数来判断类的某些对象是否已被创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream>

class MyClass {
public:
// 静态成员函数,用于判断对象是否已被创建
static bool isObjectCreated() {
return objectCount > 0;
}

// 构造函数
MyClass() {
// 对象被创建时,增加对象计数
++objectCount;
std::cout << "Object created. Total objects: " << objectCount << std::endl;
}

// 析构函数
~MyClass() {
// 对象被销毁时,减少对象计数
--objectCount;
std::cout << "Object destroyed. Total objects: " << objectCount << std::endl;
}

private:
// 静态成员变量,用于追踪对象数量
static int objectCount;
};

// 初始化静态成员变量
int MyClass::objectCount = 0;

int main() {
// 使用静态成员函数判断对象是否已被创建
std::cout << "Is object created? " << MyClass::isObjectCreated() << std::endl;

// 创建两个对象
MyClass obj1;
MyClass obj2;

// 使用静态成员函数再次判断对象是否已被创建
std::cout << "Is object created? " << MyClass::isObjectCreated() << std::endl;

// 对象会在 main 函数结束时被销毁,析构函数会减少对象计数
return 0;
}

静态成员函数与普通成员函数的区别:

  • 静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数)。
  • 普通成员函数有 this 指针,可以访问类中的任意成员;而静态成员函数没有 this 指针。

继承

继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易,也达到了重用代码功能和提高执行效率的效果。

继承代表了 is a 关系。例如,哺乳动物是动物,狗是哺乳动物,因此,狗是动物,等等。

pi0CzIx.png

1
2
3
4
5
6
7
8
9
10
11
// 基类
class Animal {
    // eat() 函数
    // sleep() 函数
};


//派生类
class Dog : public Animal {
    // bark() 函数
};

基类 & 派生类

访问修饰符 access-specifier 是 public、protectedprivate 其中的一个,base-class 是之前定义过的某个类的名称。如果未使用访问修饰符 access-specifier,则默认为 private。

派生类在继承基类的成员变量时,会单独开辟一块内存保存基类的成员变量,因此派生类自己的成员变量即使和基类的成员变量重名,但是也不会引起冲突

访问控制和继承

派生类可以访问基类中所有的非私有成员。因此基类成员如果不想被派生类的成员函数访问,则应在基类中声明为 private。

一个派生类继承了所有的基类方法,但下列情况除外:

  • 基类的构造函数、析构函数和拷贝构造函数。

    基类的构造函数 c++ 11 进行了支持,通过 using BaseClass::BaseClass; 的语句子类可以继承基类的构造函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #include <iostream>

    class BaseClass {
    public:
    BaseClass(int value) : m_value(value) {}
    void printValue() {
    std::cout << "Value: " << m_value << std::endl;
    }
    private:
    int m_value;
    };

    class DerivedClass : public BaseClass {
    public:
    using BaseClass::BaseClass; // 继承基类的构造函数
    };

    int main() {
    DerivedClass obj(10); // 使用基类的构造函数初始化派生类对象
    obj.printValue(); // 调用基类的成员函数
    return 0;
    }
  • 基类的重载运算符。

  • 基类的友元函数。

我们不能够在子类的成员函数体中调用基类的构造函数来为成员变量进行初始化。

但我们可以把基类的构造函数放在子类构造函数的初始化列表上,以此实现调用基类的构造函数来为子类从基类继承的成员变量初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>

using namespace std;

// 基类
class Shape
{
public:
Shape(int w,int h)
{
width=w;
height=h;
}
protected:
int width;
int height;
};

// 派生类
class Rectangle: public Shape
{
public:
Rectangle(int a,int b):Shape(a,b) // 这样是可以的
{

}

// Rectangle(int a,int b)
// {
// Shape(a,b); 这样是不可以的
// }
};

继承类型

当一个类派生自基类,该基类可以被继承为 public、protectedprivate 几种类型。

  • 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有保护成员来访问。
  • 保护继承(protected): 当一个类派生自保护基类时,基类的公有保护成员将成为派生类的保护成员。
  • 私有继承(private):当一个类派生自私有基类时,基类的公有保护成员将成为派生类的私有成员。

多继承

1
2
3
4
class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,…
{
<派生类类体>
};

多继承中有一个问题:菱形继承

菱形继承是一种多重继承的情况,其中一个派生类同时继承自两个直接或间接共享一个基类的派生类。

A->D, B->D, C->(A,B),意为 A 继承自 D,B 也继承自 D,而 C 继承自 A 和 B,此时可能导致二义性,编译器无法确定是应该使用 A 的 D 部分还是 B 的 D 部分。

要解决上面问题就要用虚拟继承格式

格式:class 类名: virtual 继承方式 父类名

1
2
3
4
class D{......};
class B: virtual public D{......};
class A: virtual public D{......};
class C: public B, public A{.....};

构造函数调用顺序

基类构造函数 > 类中的类成员的构造函数 > 派生类构造函数

多继承派生类: 基类构造顺序 依照 基类继承顺序调用

类成员:依照 类成员对象 定义顺序 调用成员类构造函数

重载运算符和重载函数

C++ 允许在同一作用域中的某个函数运算符指定多个定义,分别称为函数重载运算符重载

重载声明是指一个与之前已经在该作用域内声明过的函数或方法具有相同名称的声明,但是它们的参数列表和定义(实现)不相同

当您调用一个重载函数重载运算符时,编译器通过把您所使用的参数类型与定义中的参数类型进行比较,决定选用最合适的定义。选择最合适的重载函数或重载运算符的过程,称为重载决策

函数重载

同名函数的形式参数(指参数的个数、类型或者顺序)必须不同。您不能仅通过返回类型的不同来重载函数。

参数顺序不同也可以发生重载,但是要注意在给参数时要给对应的参数类型,否则会产生二义性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>

using namespace std;

class Test
{
public:
Test();
Test(int a, double b);
Test(double a, int b);

private:
int m_a;
double m_b;
};

Test::Test() : m_a(0), m_b(0.0)
{}

Test::Test(int a, double b) : m_a(a), m_b(b)
{}

Test::Test(double a, int b) : m_a(b), m_b(a)
{}

int main()
{
Test t1;
Test t2(1, 2.0);
Test t3(3.0, 45);
// Test t4(1, 2); // 二义性,编译器发现两个有参构造都符合。

return 0;
}

运算符重载

可以重定义或重载大部分 C++ 内置的运算符。这样,您就能使用自定义类型的运算符。

重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。与其他函数一样,重载运算符有一个返回类型和一个参数列表

1
Box operator+(const Box&);

大多数的重载运算符可被定义为普通的非成员函数或者被定义为类成员函数。如果我们定义上面的函数为类的非成员函数,那么我们需要为每次操作传递两个参数,如下:

1
Box operator+(const Box&, const Box&);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <iostream>
using namespace std;

class Box
{
public:

double getVolume(void)
{
return length * breadth * height;
}
void setLength( double len )
{
length = len;
}

void setBreadth( double bre )
{
breadth = bre;
}

void setHeight( double hei )
{
height = hei;
}
// 重载 + 运算符,用于把两个 Box 对象相加
Box operator+(const Box& b)
{
Box box;
box.length = this->length + b.length;
box.breadth = this->breadth + b.breadth;
box.height = this->height + b.height;
return box;
}
private:
double length; // 长度
double breadth; // 宽度
double height; // 高度
};
// 程序的主函数
int main( )
{
Box Box1; // 声明 Box1,类型为 Box
Box Box2; // 声明 Box2,类型为 Box
Box Box3; // 声明 Box3,类型为 Box
double volume = 0.0; // 把体积存储在该变量中

// Box1 详述
Box1.setLength(6.0);
Box1.setBreadth(7.0);
Box1.setHeight(5.0);

// Box2 详述
Box2.setLength(12.0);
Box2.setBreadth(13.0);
Box2.setHeight(10.0);

// Box1 的体积
volume = Box1.getVolume();
cout << "Volume of Box1 : " << volume <<endl;

// Box2 的体积
volume = Box2.getVolume();
cout << "Volume of Box2 : " << volume <<endl;

// 把两个对象相加,得到 Box3
Box3 = Box1 + Box2;

// Box3 的体积
volume = Box3.getVolume();
cout << "Volume of Box3 : " << volume <<endl;

return 0;
}

可重载运算符/不可重载运算符

下面是可重载的运算符列表:

双目算术运算符 + (加),-(减),*(乘),/(除),% (取模)
关系运算符 ==(等于),!= (不等于),< (小于),> (大于),<=(小于等于),>=(大于等于)
逻辑运算符 ||(逻辑或),&&(逻辑与),!(逻辑非)
单目运算符 + (正),-(负),*(指针),&(取地址)
自增自减运算符 ++(自增),–(自减)
位运算符 | (按位或),& (按位与),~(按位取反),^(按位异或),,<< (左移),>>(右移)
赋值运算符 =, +=, -=, *=, /= , % = , &=, |=, ^=, <<=, >>=
空间申请与释放 new, delete, new[ ] , delete[]
其他运算符 ()(函数调用),**->(成员访问),,(逗号),[]**(下标)

下面是不可重载的运算符列表:

  • .:成员访问运算符
  • .*, ->*:成员指针访问运算符
  • :::域运算符
  • sizeof:长度运算符
  • ?::条件运算符
  • #: 预处理符号

成员指针访问运算符

.*->* 是C++中用于操作成员指针的成员指针访问运算符。这两个运算符用于通过成员指针访问类的成员,特别是在涉及到继承和多态的情况下。

  1. .* 运算符:

    • .* 运算符用于通过对象指针或引用访问成员变量或成员函数。

    • 语法:object.*memberPointerobjectReference.*memberPointer

    • object 是对象的指针或引用,memberPointer 是成员指针。

    • 示例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      #include <iostream>

      class MyClass {
      public:
      int data;
      void display() {
      std::cout << "Data: " << data << std::endl;
      }
      };

      int main() {
      MyClass obj;
      obj.data = 42;

      int MyClass::*dataMemberPointer = &MyClass::data;
      void (MyClass::*functionMemberPointer)() = &MyClass::display;

      std::cout << "Using .* to access data member: " << obj.*dataMemberPointer << std::endl;
      (obj.*functionMemberPointer)(); // Using .* to call member function

      return 0;
      }
  2. ->* 运算符:

    • ->* 运算符用于通过对象指针访问成员变量或成员函数。

    • 语法:objectPointer->*memberPointer

    • objectPointer 是指向对象的指针,memberPointer 是成员指针。

    • 示例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      #include <iostream>

      class MyClass {
      public:
      int data;
      void display() {
      std::cout << "Data: " << data << std::endl;
      }
      };

      int main() {
      MyClass obj;
      obj.data = 42;

      MyClass* objPointer = &obj;

      int MyClass::*dataMemberPointer = &MyClass::data;
      void (MyClass::*functionMemberPointer)() = &MyClass::display;

      std::cout << "Using ->* to access data member: " << (objPointer->*dataMemberPointer) << std::endl;
      (objPointer->*functionMemberPointer)(); // Using ->* to call member function

      return 0;
      }

这两个运算符对于处理成员指针非常有用,特别是在某些需要运行时动态选择成员的情况下,例如在基类和派生类之间切换。

运算符重载实例

序号 运算符和实例
1 一元运算符重载
2 二元运算符重载
3 关系运算符重载
4 输入/输出运算符重载
5 ++ 和 – 运算符重载
6 赋值运算符重载
7 函数调用运算符 () 重载
8 下标运算符 [] 重载
9 类成员访问运算符 -> 重载

运算符重载的同时也可以发生函数重载

也就是说,运算符重载时,根据给的参数类型、个数等不同,也可以被重载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include<iostream>

using namespace std;

//加号运算符重载
class xiMeng
{
public:
int M_A;
int M_B;
//通过成员函数运算符重载
/*xiMeng operator + (xiMeng& p)
{
xiMeng temp;
temp.M_A = this->M_A + p.M_A;
temp.M_B = this->M_B + p.M_B;
return temp;
}*/
};

//通过全局函数运算符重载
xiMeng operator+ (xiMeng& p1, xiMeng& p2)
{
xiMeng temp;
temp.M_A = p1.M_A + p2.M_A;
temp.M_B = p1.M_B + p2.M_B;
return temp;
}

//运算符重载也可以发生函数重载
xiMeng operator+ (xiMeng& p, int num)
{
xiMeng temp;
temp.M_A = p.M_A + num;
temp.M_B = p.M_B + num;
return temp;
}


void xiMengTest() {
xiMeng p1;
p1.M_A = 15;
p1.M_B = 25;

xiMeng p2;
p2.M_A = 10;
p2.M_B = 30;

//通过全局函数运算符重载
xiMeng p3 = p1 + p2;
cout << "p3.M_A = " << p3.M_A << endl;
cout << "p3.M_B = " << p3.M_B << endl;

//运算符重载也可以发生函数重载
xiMeng p4 = p1 + 100;
cout << "p4.M_A = " << p4.M_A << endl;
cout << "p4.M_B = " << p4.M_B << endl;
}

int main()
{
xiMengTest();

return 0;
}

多态

当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。

C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数

有了多态,您可以有多个不同的类,都带有同一个名称但具有不同实现的函数,函数的参数甚至可以是相同的。

形成多态的条件:

  1. 必须存在继承关系;

  2. 继承关系必须有同名虚函数(其中虚函数是在基类中使用关键字Virtual声明的函数,在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数);

  3. 存在基类类型的指针或者引用,通过该指针或引用调用虚函数;

静态多态(静态链接,早绑定)

函数调用在程序执行前就准备好了。即导致子类对象调用与父类相同的函数,却依然调用了父类的函数,此时就需要虚函数关键字 virtual 实现晚绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <iostream> 
using namespace std;

class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0)
{
width = a;
height = b;
}
int area()
{
cout << "Parent class area :" <<endl;
return 0;
}
};
class Rectangle: public Shape{
public:
Rectangle( int a=0, int b=0):Shape(a, b) { }
int area ()
{
cout << "Rectangle class area :" <<endl;
return (width * height);
}
};
class Triangle: public Shape{
public:
Triangle( int a=0, int b=0):Shape(a, b) { }
int area ()
{
cout << "Triangle class area :" <<endl;
return (width * height / 2);
}
};
// 程序的主函数
int main( )
{
Shape *shape;
Rectangle rec(10,7);
Triangle tri(10,5);

// 存储矩形的地址
shape = &rec;
// 调用矩形的求面积函数 area
shape->area();

// 存储三角形的地址
shape = &tri;
// 调用三角形的求面积函数 area
shape->area();

return 0;
}

结果:

1
2
Parent class area :
Parent class area :

动态多态(动态链接,晚绑定)

将基类中的函数修改为虚函数后:

编译器看的是指针的内容,而不是它的类型。因此,由于 tri 和 rec 类的对象的地址存储在 *shape 中,所以会调用各自的 area() 函数。

每个子类都有一个函数 area() 的独立实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0)
{
width = a;
height = b;
}
virtual int area()
{
cout << "Parent class area :" <<endl;
return 0;
}
};

结果:

1
2
Rectangle class area :
Triangle class area :

虚函数

只需要在函数声明中使用 virtual 关键字,定义不需要。

调用时可以在函数之前加基类和作用域符就可以调用基类的虚函数。

虚函数 是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。

在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定

纯虚函数

在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。

= 0 告诉编译器,函数没有主体,下面的虚函数是纯虚函数

抽象类不可以创建实例化对象,但是可以创建指针或引用,从而指向派生类对象。

1
2
3
4
5
6
7
8
9
10
11
12
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0)
{
width = a;
height = b;
}
// pure virtual function
virtual int area() = 0;
};

多态中的析构函数

在多态中,需要将基类的析构函数声明为 virtual 的,这样才能确保正确调用派生类的析构函数,然后再调用基类的析构函数。

在多态情境下,确保正确释放资源的关键是将析构函数声明为虚函数。当通过基类指针删除指向派生类对象的内存时,如果析构函数不是虚函数,只会调用基类的析构函数,而不会调用派生类的析构函数。这可能导致派生类中的资源没有正确释放,从而产生内存泄漏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>

class Base {
public:
virtual ~Base() { // 虚析构函数
std::cout << "Base Destructor" << std::endl;
}

virtual void display() const {
std::cout << "Display from Base" << std::endl;
}
};

class Derived : public Base {
public:
~Derived() override {
std::cout << "Derived Destructor" << std::endl;
}

void display() const override {
std::cout << "Display from Derived" << std::endl;
}
};

int main() {
Base* polymorphicObject = new Derived();

polymorphicObject->display();

delete polymorphicObject; // 通过基类指针释放内存

return 0;
}

关于多态的一些知识点

  1. 纯虚函数(virtual void funtion1()=0;)用于规范派生类的行为,即接口。包含纯虚函数的类是抽象类,抽象类不能定义实例,但可以声明指向实现该抽象类的具体类的指针或引用。
  2. 虚函数声明如下:virtual ReturnType FunctionName(Parameter) 虚函数必须实现,如果不实现,编译器将报错。
  3. 对于虚函数来说,父类和子类都有各自的版本。由多态方式调用的时候动态绑定。
  4. 实现了纯虚函数的子类,该纯虚函数在子类中就变成了虚函数,子类的子类即孙子类可以覆盖该虚函数,由多态方式调用的时候动态绑定。
  5. 虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数(通过基类的指针或引用指向派生类对象)。
  6. 在有动态分配堆上内存的时候,析构函数必须是虚函数,但没有必要是纯虚的。
  7. 友元不是成员函数,只有成员函数才可以是虚拟的,因此友元不能是虚拟函数。但可以通过让友元函数调用虚拟成员函数来解决友元的虚拟问题。
  8. 析构函数应当是虚函数,将调用相应对象类型的析构函数,因此,如果指针指向的是子类对象,将调用子类的析构函数,然后自动调用基类的析构函数。

声明一个指向抽象类的指针和引用的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>

// 抽象类
class AbstractClass {
public:
// 纯虚函数
virtual void pureVirtualFunction() const = 0;

// 普通成员函数
void regularFunction() const {
std::cout << "Regular function in AbstractClass." << std::endl;
}
};

// 派生类
class DerivedClass : public AbstractClass {
public:
// 实现纯虚函数
void pureVirtualFunction() const override {
std::cout << "Pure virtual function in DerivedClass." << std::endl;
}
};

int main() {
// 声明指向抽象类的指针
AbstractClass* abstractPtr = nullptr;

// 声明指向抽象类的引用
AbstractClass& abstractRef = *abstractPtr; // 注意:在使用前确保指针不为 nullptr

// 使用指针或引用访问抽象类的成员函数
abstractPtr = new DerivedClass(); // 可以将指向派生类对象的指针赋值给指向抽象类的指针
abstractPtr->pureVirtualFunction(); // 调用纯虚函数
abstractPtr->regularFunction(); // 调用普通成员函数

// 释放内存
delete abstractPtr;

return 0;
}

override 和 final

在C++11中,overridefinal 是用于增强代码可读性和安全性的两个关键字,主要用于虚函数的声明和继承关系。

  1. override 关键字:

    放在派生类需要重写的函数后,也就是表明这是一个需要被重写的虚函数。

    • override 关键字用于显式指示派生类中的成员函数要重写基类中的虚函数。

    • 当使用 override 关键字时,编译器会检查该函数是否真的是基类中虚函数的重写,如果不是,将产生编译错误。

    • 示例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      class Base {
      public:
      virtual void foo() const {
      // 基类虚函数
      }
      };

      class Derived : public Base {
      public:
      void foo() const override {
      // 派生类重写基类虚函数
      }
      };
  2. final 关键字:

    • final 关键字用于阻止类的继承或虚函数的进一步重写。

    • 在类声明或虚函数声明后面加上 final 关键字,表示该类不允许被继承,或者表示该虚函数不允许在派生类中被重写。

    • 示例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      class Base final {
      // final 关键字阻止该类被继承
      };

      class Derived : public Base {
      // 这里会导致编译错误,因为 Base 被声明为 final,不允许继承
      };

      class A {
      public:
      virtual void func() const final {
      // final 关键字阻止该虚函数在派生类中被重写
      }
      };

      class B : public A {
      func(){}// 这里会导致编译错误,因为 func 被声明为 final,不允许重写
      };
    • 注意:final 关键字只能用于类或者虚函数,而不能用于普通的非虚函数。

这两个关键字有助于提高代码的可读性和安全性,避免一些潜在的错误,特别是在大型代码库中。通过明确标记重写和禁止继承,程序员可以更容易地理解代码的意图,并避免一些潜在的错误和不必要的复杂性。

数据抽象

数据抽象是指,只向外界提供关键信息,并隐藏其后台的实现细节,即只表现必要的信息而不呈现细节。

在 C++ 中,我们使用来定义我们自己的抽象数据类型(ADT)。

也就是说,使用 public 标签定义对外接口,private 标签定义类内细节,避免全部成员变量为 public,导致出现问题后,所有直接访问的都出现问题。

设计策略

抽象把代码分离为接口和实现。所以在设计组件时,必须保持接口独立于实现,这样,如果改变底层实现,接口也将保持不变。

在这种情况下,不管任何程序使用接口,接口都不会受到影响,只需要将最新的实现重新编译即可。

数据封装

封装是面向对象编程中的把数据和操作数据的函数绑定在一起的一个概念,这样能避免受到外界的干扰和误用,从而确保了安全。

数据封装是一种把数据和操作数据的函数捆绑在一起的机制,数据抽象是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制。

C++ 通过创建来支持封装和数据隐藏(public、protected、private)。

通常情况下,我们都会设置类成员状态为私有(private),除非我们真的需要将其暴露,这样才能保证良好的封装性。这通常应用于数据成员,但它同样适用于所有成员,包括虚函数。

C++中**, 虚函数**可以为private, 并且可以被子类覆盖(因为虚函数表的传递),但子类不能调用父类的private虚函数。虚函数的重载性和它声明的权限无关。

纯虚函数可以设计成私有的,不过这样不允许在本类之外的非友元函数中直接调用它,子类中只有覆盖这种纯虚函数的义务,却没有调用它的权利。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>

class Base {
private:
virtual void privateVirtualFunction() {
std::cout << "Base::privateVirtualFunction()" << std::endl;
}

public:
// 公有接口调用私有虚函数
void callPrivateVirtualFunction() {
std::cout << "Calling private virtual function from Base class." << std::endl;
privateVirtualFunction();
}

// 公有接口,供子类重写
void publicInterface() {
std::cout << "Base::publicInterface()" << std::endl;
privateVirtualFunction(); // 在基类中调用虚函数
}
};

class Derived : public Base {
private:
// 重写基类的私有虚函数
void privateVirtualFunction() override {
std::cout << "Derived::privateVirtualFunction()" << std::endl;
}

public:
// 重写基类的公有接口
void publicInterface() override {
std::cout << "Derived::publicInterface()" << std::endl;
privateVirtualFunction(); // 在派生类中调用虚函数
}
};

int main() {
Base baseObj;
Derived derivedObj;

baseObj.callPrivateVirtualFunction(); // 在基类中调用私有虚函数
baseObj.publicInterface(); // 在基类中调用公有接口

derivedObj.callPrivateVirtualFunction(); // 在派生类中调用私有虚函数
derivedObj.publicInterface(); // 在派生类中调用公有接口

return 0;
}

接口(抽象类)

接口描述了类的行为和功能,而不需要完成类的特定实现。

C++ 接口是使用抽象类来实现的,抽象类与数据抽象互不混淆,数据抽象是一个把实现细节与相关的数据分离开的概念。

如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。纯虚函数是通过在声明中使用 “= 0” 来指定的。

设计抽象类(通常称为 ABC)的目的,是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,它只能作为接口使用。如果试图实例化一个抽象类的对象,会导致编译错误。

因此,如果一个 ABC 的子类需要被实例化,则必须实现每个纯虚函数,这也意味着 C++ 支持使用 ABC 声明接口。如果没有在派生类中重写纯虚函数,就尝试实例化该类的对象,会导致编译错误。

可用于实例化对象的类被称为具体类

设计策略

面向对象的系统可能会使用一个抽象基类为所有的外部应用程序提供一个适当的、通用的、标准化的接口。然后,派生类通过继承抽象基类,就把所有类似的操作都继承下来。

外部应用程序提供的功能(即公有函数)在抽象基类中是以纯虚函数的形式存在的。这些纯虚函数在相应的派生类中被实现。

规范:

定义一个函数为虚函数,不代表函数为不被实现的函数。

定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。

定义一个函数为纯虚函数,才代表函数没有被实现。

定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。

文件和流

标准库 fstream 中的三种数据类型。

数据类型 描述
ofstream 该数据类型表示输出文件流,用于创建文件并向文件写入信息。
ifstream 该数据类型表示输入文件流,用于从文件读取信息。
fstream 该数据类型通常表示文件流,且同时具有 ofstream 和 ifstream 两种功能,这意味着它可以创建文件,向文件写入信息,从文件读取信息。

打开文件

在从文件读取信息或者向文件写入信息之前,必须先打开文件。ofstreamfstream 对象都可以用来打开文件进行写操作,如果只需要打开文件进行读操作,则使用 ifstream 对象。

1
void open(const char *filename, ios::openmode mode);

open() 成员函数的第一参数指定要打开的文件的名称和位置,第二个参数定义文件被打开的模式(模式标志可以结合使用 ios::out | ios::trunc)。

模式标志 描述
ios::app 追加模式。所有写入都追加到文件末尾。
ios::ate 文件打开后定位到文件末尾。
ios::in 打开文件用于读取。
ios::out 打开文件用于写入。
ios::trunc 如果该文件已经存在,其内容将在打开文件之前被截断,即把文件长度设为 0。

在C++中,ofstreamifstream 都是文件流类,分别用于输出文件(写入数据到文件)和输入文件(从文件读取数据)。它们都是基于 ostreamistream 类派生而来的,分别提供了用于写入和读取文件的功能。

  1. ofstream(Output File Stream)

    • 用于创建文件并将数据写入文件。
    • 如果文件不存在,会创建一个新文件;如果文件已存在,会清空文件的内容并从头开始写。
    • 示例:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      #include <fstream>
      #include <iostream>

      int main() {
      std::ofstream outputFile("example.txt");
      if (outputFile.is_open()) {
      outputFile << "Hello, ofstream!";
      outputFile.close();
      } else {
      std::cerr << "Unable to open the file." << std::endl;
      }

      return 0;
      }
  2. ifstream(Input File Stream)

    • 用于从文件读取数据。
    • 如果文件不存在或无法打开,会导致失败。
    • 示例:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      #include <fstream>
      #include <iostream>

      int main() {
      std::ifstream inputFile("example.txt");
      if (inputFile.is_open()) {
      std::string line;
      while (std::getline(inputFile, line)) {
      std::cout << line << std::endl;
      }
      inputFile.close();
      } else {
      std::cerr << "Unable to open the file." << std::endl;
      }

      return 0;
      }

总的来说,ofstreamifstream 主要区别在于它们的用途,一个用于写入文件,一个用于读取文件。同时,它们都属于文件流类,因此都继承自 ostreamistream,共享一些相似的特性和接口。

  1. fstream 是 C++ 文件流类的通用基类,可以用于创建文件并进行读写操作。fstream 类同时继承了 ifstream(用于输入文件流)和 ofstream(用于输出文件流)的功能,因此可以在同一个对象上执行读写操作。

关闭文件

1
void close();

写入文件

向屏幕输出使用 cout 对象,向文件输出,则使用文件对象(通过文件对象调用函数);输入同理。

cin.getline 和 std::getline

区别在于 cin.getline 是将获取的数据存到字符数组;std::getline 则是将获取的数据存到 string 类。

cin.getline

cin.getline 是 C++ 标准库中 istream 类的成员函数之一,用于从输入流中读取一行数据,并将其存储为字符数组(C-Style 字符串)。这个函数和 std::getline 有一些区别,主要是 cin.getline 需要指定缓冲区的大小,而 std::getline 则使用 std::string 类型的对象,动态调整大小。

以下是对 cin.getline 函数的简要讲解:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>

int main() {
const int bufferSize = 100; // 指定缓冲区大小
char userInput[bufferSize];

std::cout << "Enter a line of text: ";
std::cin.getline(userInput, bufferSize);

std::cout << "You entered: " << userInput << std::endl;

return 0;
}

在这个例子中,std::cin.getline 函数从标准输入 (std::cin) 读取一行文本,并将其存储在大小为 bufferSize 的字符数组 userInput 中。与 std::getline 不同,cin.getline 需要提供字符数组的大小。

主要特点和用法如下:

  1. std::cin.getline 函数的签名

    1
    istream& getline (char* s, streamsize count, char delim = '\n');
    • s 是指向字符数组的指针,用于存储输入的文本。
    • count 是字符数组的大小,表示最多读取的字符数。
    • delim 是可选的分隔符,默认是换行符 \n。如果指定了分隔符,getline 会在遇到分隔符之前读取输入。
  2. 读取一行文本

    1
    2
    3
    const int bufferSize = 100;
    char userInput[bufferSize];
    std::cin.getline(userInput, bufferSize);

    这行代码从标准输入中读取一行文本,最多读取 bufferSize - 1 个字符,以确保在数组末尾添加 null 终止符。

  3. 指定分隔符

    1
    2
    3
    const int bufferSize = 100;
    char userInput[bufferSize];
    std::cin.getline(userInput, bufferSize, ',');

    这行代码指定了逗号 , 作为分隔符,表示 getline 会在遇到逗号之前读取输入。

std::cin.getline 是一个比较底层的输入函数,适用于处理字符数组的场景。然而,如果可能的话,推荐使用 std::getlinestd::string,因为它们更具有灵活性和安全性。

std::getline

getline 是 C++ 标准库中 istream 类的成员函数,用于从输入流中读取一行数据。它通常与 cin 对象一起使用,以便从标准输入读取用户输入的一行文本。

  1. std::getline 函数的签名

    1
    istream& getline (istream& is, string& str, char delim = '\n');
    • is 是输入流对象,这里通常是 std::cin
    • str 是用于存储输入行的字符串对象。
    • delim 是可选的分隔符,默认是换行符 \n。如果指定了分隔符,getline 会在遇到分隔符之前读取输入。
  2. 读取一行文本

    1
    std::getline(std::cin, userInput);

    这行代码从标准输入中读取一行文本,直到遇到换行符为止,并将其存储在 userInput 字符串中。

  3. 指定分隔符

    1
    std::getline(std::cin, userInput, ',');

    这行代码指定了逗号 , 作为分隔符,表示 getline 会在遇到逗号之前读取输入。

std::getline 是处理用户输入的一种常见方式,特别是在需要读取整行文本的情况下。它允许输入包含空格的字符串,并且可以用于读取以特定字符为分隔符的文本。

文件位置指针

输入流(ifstream)中:get(char ch) 用于读取一个字符,并且使用 get 后的文件指针位置为获取到的字符的下一位。tellg 获取 get 指针当前位置。seekg 设置 get 指针位置。

输出流(ofstream)中:put(char ch) 用于写入一个字符,并且使用 put 后的文件指针位置为写入的字符的下一位。tellp 函数用于获取 put 指针的当前位置。seekp 函数用于设置 put 指针的位置。

C++ 的文件位置指针是用于在文件中定位当前读/写位置的机制。在 C++ 中,文件位置指针通常与文件流类一起使用,主要有三种类型的文件位置指针:get 指针、put 指针和文件指针。这些指针用于输入、输出和同时进行输入输出操作。

  1. get 指针

    • get 指针用于定位输入位置。对于输入流(如 ifstreamfstream),get 指针指示从文件中读取下一个字符的位置。
    • tellg 函数用于获取 get 指针的当前位置。
    • seekg 函数用于设置 get 指针的位置。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    #include <iostream>
    #include <fstream>

    int main() {
    std::ifstream inputFile("example.txt");

    if (inputFile.is_open()) {
    char ch;
    inputFile.get(ch); // 读取一个字符
    std::cout << "Read character: " << ch << std::endl;

    std::streampos currentPosition = inputFile.tellg(); // 获取当前位置
    std::cout << "Current position: " << currentPosition << std::endl;

    inputFile.seekg(0, std::ios::beg); // 设置到文件开头
    inputFile.get(ch); // 再次读取一个字符
    std::cout << "Read character after seeking to the beginning: " << ch << std::endl;

    inputFile.close();
    } else {
    std::cerr << "Unable to open the file." << std::endl;
    }

    return 0;
    }
  2. put 指针

    • put 指针用于定位输出位置。对于输出流(如 ofstreamfstream),put 指针指示下一个字符将被写入的位置。
    • tellp 函数用于获取 put 指针的当前位置。
    • seekp 函数用于设置 put 指针的位置。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #include <iostream>
    #include <fstream>

    int main() {
    std::ofstream outputFile("example.txt");

    if (outputFile.is_open()) {
    outputFile.put('A'); // 写入一个字符
    std::streampos currentPosition = outputFile.tellp(); // 获取当前位置
    std::cout << "Current position: " << currentPosition << std::endl;

    outputFile.seekp(5, std::ios::beg); // 设置到离文件开头 5 个字节的位置
    outputFile.put('B'); // 在新位置写入字符
    std::cout << "Wrote character after seeking: " << 'B' << std::endl;

    outputFile.close();
    } else {
    std::cerr << "Unable to open the file." << std::endl;
    }

    return 0;
    }
  3. 文件指针

    • 文件指针是同时用于输入和输出操作的指针。在 fstream 中,文件指针可以用于同时定位输入和输出位置。
    • tellptellg 用于获取文件指针的当前位置。
    • seekpseekg 用于设置文件指针的位置。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    #include <iostream>
    #include <fstream>

    int main() {
    std::fstream fileStream("example.txt", std::ios::in | std::ios::out | std::ios::trunc);

    if (fileStream.is_open()) {
    fileStream << "1234567890"; // 写入数据

    std::streampos currentPosition = fileStream.tellp(); // 获取当前写入位置
    std::cout << "Current write position: " << currentPosition << std::endl;

    fileStream.seekg(3, std::ios::beg); // 设置读取位置到离文件开头 3 个字节的位置
    int value;
    fileStream >> value; // 读取整数
    std::cout << "Read value after seeking: " << value << std::endl;

    fileStream.close();
    } else {
    std::cerr << "Unable to open the file." << std::endl;
    }

    return 0;
    }

在这些例子中,使用 tellgtellp 函数获取当前位置,使用 seekgseekp 函数设置文件指针的位置。这些函数的第一个参数是偏移量,第二个参数是 seek 的基准位置,可以是 std::ios::beg(文件开头)、std::ios::cur(当前位置)或 std::ios::end(文件末尾)。

seekg 和 seekp

ios_base 是 ios 的基类,一般使用派生类,即 ios::

seekgseekp 是 C++ 标准库中 istreamostream 类的成员函数,用于设置文件位置指针。这两个函数分别用于在输入流和输出流上设置位置指针。

seekg 函数:

1
2
istream& seekg (streampos pos);
istream& seekg (streamoff off, ios_base::seekdir way);
  • pos 版本: 第一个版本将 istream 对象的 get 指针设置为 pospos 是类型为 streampos 的参数,表示绝对位置。

  • off 版本: 第二个版本将 istream 对象的 get 指针移动 off 个字节,移动方向由 way 参数指定。

    • off:表示偏移量,可以为正或负值。
    • way:指定移动的基准位置,可以是 ios_base::beg(文件开头)、ios_base::cur(当前位置)或 ios_base::end(文件末尾)。

seekp 函数:

1
2
ostream& seekp (streampos pos);
ostream& seekp (streamoff off, ios_base::seekdir way);
  • pos 版本: 第一个版本将 ostream 对象的 put 指针设置为 pospos 是类型为 streampos 的参数,表示绝对位置。

  • off 版本: 第二个版本将 ostream 对象的 put 指针移动 off 个字节,移动方向由 way 参数指定。

    • off:表示偏移量,可以为正或负值。
    • way:指定移动的基准位置,可以是 ios_base::beg(文件开头)、ios_base::cur(当前位置)或 ios_base::end(文件末尾)。

示例:

1
2
3
4
5
6
7
8
9
10
11
// 定位到 fileObject 的第 n 个字节(假设是 ios::beg)
fileObject.seekg( n );

// 把文件的读指针从 fileObject 当前位置向后移 n 个字节
fileObject.seekg( n, ios::cur );

// 把文件的读指针从 fileObject 末尾往回移 n 个字节
fileObject.seekg( n, ios::end );

// 定位到 fileObject 的末尾
fileObject.seekg( 0, ios::end );

cin.ignore

cin.ignore 是 C++ 标准库中 istream 类的成员函数之一,用于忽略输入流中的字符。它的主要用途是清除输入缓冲区中的字符,以便进行下一次输入操作。cin.ignore 可以接受一个或两个参数,它们控制函数的行为。

函数原型:

1
istream& ignore (streamsize n = 1, int delim = EOF);
  • n 参数: 表示要忽略的字符数。默认值为 1,即默认忽略一个字符。如果指定了 n,则函数将忽略输入流中的下一个 n 个字符。

  • delim 参数: 表示定界符(delimiter),默认值为 EOF(End of File)。如果指定了 delim,则 ignore 函数将忽略输入流中的字符,直到遇到定界符,并将该定界符忽略为止。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

int main() {
int value;
char newline;

std::cout << "Enter an integer: ";
std::cin >> value;

// 忽略输入缓冲区中的换行符
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');

std::cout << "Enter a string: ";
std::string str;
std::getline(std::cin, str);

std::cout << "You entered: " << value << " and " << str << std::endl;

return 0;
}

在这个例子中,用户首先被要求输入一个整数。然后,通过 cin.ignore 忽略了输入缓冲区中的换行符,以确保接下来的 std::getline 函数可以正确读取用户输入的字符串。这是因为之前使用 std::cin >> value 时,用户按下 Enter 键会在输入缓冲区中留下一个换行符。如果没有使用 cin.ignore,这个换行符将被视为 std::getline 的输入,导致输入不符合预期。