avatar

Ycyofmine’s Blog

云鸮雨霁

  • 首页
  • 分类
  • 标签
  • 归档
  • 关于
首页 C++ Primer(正在更新)
文章

C++ Primer(正在更新)

发表于 2024/10/23
作者 Ycyofmine
24 分钟阅读

写在前面

有些我认为过于基础,我已经会的东西不会进行记录。

第 Ⅰ 部分 C++基础

第二章 变量和基本类型

const限定符

const修饰的变量初始化,可以利用一个非const对象来赋值,它会将值拷贝过来,就和原来的对象无关了。

const对象仅在文件内有效,多个文件有同名const对象,其实都是独立的变量,如果想要多文件同名const对象是一个对象,应都加上 extern 。

初始化和对const的引用

1
2
double dval = 3.14;
const int &ri = dval;

此处对ri的操作应该是 int ,但他是 double ,所以编译器会将上述代码变成:

1
2
const int temp = dval;
const int &ri = temp;

接下来讨论当ri不是常量时,此时ri绑定的是临时量,而不是dval,这是一种非法行为。

顶层const

顶层const表示对象本身不可修改,底层const表示对象指向的对象不可修改。

  • 顶层 const 主要用于限定变量的不可修改性,它常用于定义常量,如常量整数或常量指针。
  • 底层 const 主要用于保护通过指针或引用访问的对象,使得不能通过这些指针或引用修改对象的值。这在函数形参或接口设计中非常重要,确保数据不会被意外修改。

const练习

1
2
3
4
5
6
7
const int v2 = 0; // v2 是顶层 const,表示 v2 的值不可修改。
int v1 = v2;      // v1 是普通的 int 类型,可以修改。
int *p1 = &v1, &r1 = v1; // p1 是指向 v1 的指针,v1 的值可以通过 p1 修改;
                        //r1 是 v1 的引用,同样可以修改 v1 的值。
const int *p2 = &v2, *const p3 = &i, &r2 = v2; // p2 是底层 const,表示不能通过 p2 修改 v2 的值;
                                                //p3 是顶层和底层 const,既不能修改 p3 指向的对象,也不能改变 p3 本身的值;
                                                //r2 是底层 const,不能通过 r2 修改 v2 的值。
1
2
3
4
5
r1 = v2;   // 合法,r1 是 v1 的引用,r1 的值可以被赋值。
p1 = p2;   // 不合法,p1 是 `int*` 类型,p2 是 `const int*`,不能将指向 `const` 对象的指针赋给普通指针。
p2 = p1;   // 合法,p2 是 `const int*`,允许将普通指针赋值给 `const int*`,因为 p2 保证不修改值。
p1 = p3;   // 不合法,p1 是 `int*` 类型,p3 是 `const int*`,不能将指向 `const` 对象的指针赋给普通指针。
p2 = p3;   // 合法,p2 和 p3 都是 `const int*` 类型,可以相互赋值。

第五章 语句

TRY语句和异常处理

当程序的某部分检测到一个它无法处理的问题时,就需要用到异常处理。

在C++语言中,异常处理包括:

  • throw表达式
  • try语句块,以关键字 try 开始,并以一个或多个 catch 子句结束。 try 语句块中代码抛出的异常通常会被某个catch子句处理。
  • 异常类 用于在 throw 表达式和相关的 catch 子句间传递异常的具体信息。

throw表达式

抛出异常将终止当前的函数,类比 return 。

try语句块

程序进入某个 try 语句块,处理异常之后程序会跳到 try 语句块最后一个 catch 子句之后的语句。

  • 函数在寻找处理代码的过程中退出

    例如,一个 try 语句块可能调用了包含另一个 try 语句块的函数,新的 try 语句块可能调用了包含又一个 try 语句块的新函数,以此类推。

    寻找处理代码的过程与函数调用链刚好相反。当异常被抛出时,首先搜索抛出该异常的函数。如果没找到匹配的 catch 子句,终止该函数,并在调用该函数的函数中继续寻 找。如果还是没有找到匹配的 catch 子句,这个新的函数也被终止,继续搜索调用它的 函数。以此类推,沿着程序的执行路径逐层回退,直到找到适当类型的 catch 子句为止。类比回溯

    如果最终还是没能找到任何匹配的 catch 子句,程序转到名为 terminate 的标准库函数。一般执行该函数将导致程序非正常退出。

编写异常安全的代码非常困难。

在异常发生期间正确执行了“清理”工作的代码被称为异常安全的代码,非常难编写。

标准异常

异常类型只有一个名为 what 的成员函数,没有任何参数,返回值是一个 const char*

函数

函数基础

函数声明

我们建议变量在头文件中声明,在源文件中定义。与之类似,函数也应该在头文件中声明而在源文件中定义。

我们把函数声明在头文件中,就能确保同一函数的所有声明保持一致。而且一旦我们想改变函数的接口,只需要改变一条声明即可。

1
2
3
4
5
6
7
8
// geometry.h
#ifndef GEOMETRY_H
#define GEOMETRY_H

// 函数声明
double calculateArea(double width, double height);

#endif
1
2
3
4
5
6
7
// geometry.cpp
#include "geometry.h"

// 函数实现
double calculateArea(double width, double height) {
    return width * height;
}
1
2
3
4
5
6
7
8
9
// main.cpp
#include "geometry.h"
#include <iostream>

int main() {
    double width = 5.0, height = 3.0;
    std::cout << "Area: " << calculateArea(width, height) << std::endl;
    return 0;
}

想改变 calculateArea 的返回类型就只要改变 geometry.h 中的声明。

参数传递

main:处理命令行选项

假设 main 函数位于可执行文件 prog 之内,我们可以向程序传递下面的选项:

1
prog -d -o ofile data0
1
int main(int argc, char *argv[]) {}

以上面提供的命令为例:

1
2
3
4
5
argv[0] = "prog"//程序名字,也可以指向空字符串
argv[1] = "-d"
argv[2] = "-o"

·······

含有可变形参的函数

initializer_list 与 vector 异同点。

  • 同:都是模板类型,定义时必须说明所含元素类型。
  • 异:initializer_list 中的元素永远是常量值。

特殊用途语言特性

constexpr

定义 constexpr 函数的方法要遵循几项约定:

  • 函数的返回类型及所有的形参的类型都得是字面值类型
  • 函数体中必须有且只有一条 return 语句:
    1
    2
    
      constexpr int new_sz() { return 42; }
      constexpr int foo = new_sz();
    

编译器把 constepxr 的调用替换成结果值。constepxr 函数被隐式地指定为内联函数。

需要注意的是 constepxr 函数不一定返回常量表达式。

调试帮助

NDEBUG 预处理变量

assert 的行为依赖于 NDEBUG ,如果定义了 NDEBUG ,assert 就会什么都不做。

可以用于编写自己的条件调试代码:通过 ifndef 和 endif 来控制输出。

  • _ _func_ _ 当前调试的函数的名字
  • _ _FILE_ _ 当前文件名
  • _ _LINE_ _ 当前行号
  • _ _TIME_ _ 当前文件的编译时间
  • _ _DATE_ _ 当前文件的编译日期

函数指针

bool (*pf)(const string &, const string &)

主要*pf一定要有括号。

当我们使用重载函数时,上下文必须清晰地界定到底应该选择调用哪个函数。如果定义了指向重载函数的指针:

  • 重载函数的指针

    1
    2
    3
    4
    
      void ff(int*);
      void ff(unsigned int*);
    
      void (*pf1)(unsigned int*) = ff; // pf1指向ff(unsigned)
    

    编译器通过指针类型决定选用哪个函数,指针类型必须与重载函数中的某一个精确匹配。

    1
    2
    
      void (*pf2)(int) = ff;   // 错误:没有任何一个ff与该形参列表匹配
      double (*pf3)(int*) = ff; // 错误:ff和pf3的返回类型不匹配
    
  • 函数指针的形参

    1
    2
    3
    
      //第三个形参是函数类型,它会自动地转换成指向函数的指针
      void useBigger(const string &s1, const string &s2, 
                      bool pf(const string &, const string &))
    
    1
    2
    3
    
      //显示地将形参定义成指向函数的指针
      void useBigger(const string &s1, const string &s2, 
                      bool (*pf)(const string &, const string &))
    

类

访问控制与封装

类的作者常常需要定义一些辅助函数,尽管这些函数定义的操作从概念上属于类的接口的组成部分,但它们实际上并不属于类本身。

定义非成员函数的方式与定义普通函数一样。通常,这些函数的声明和定义是分开的,声明会放在一个头文件中,以便用户在其他地方调用时可以找到这个接口。

实例如下:

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
// Point.h 头文件
#ifndef POINT_H
#define POINT_H

#include <iostream>

// 定义 Point 类
class Point {
public:
    Point(int x, int y) : x_(x), y_(y) {}

    // 友元声明,以允许非成员函数访问私有成员
    friend Point add(const Point& p1, const Point& p2);
    friend void print(const Point& p);

private:
    int x_, y_;  // 点的 x 和 y 坐标
};

// 非成员函数:add
// 定义一个与 Point 类相关的函数,但不属于类本身
Point add(const Point& p1, const Point& p2) {
    return Point(p1.x_ + p2.x_, p1.y_ + p2.y_);
}

// 非成员函数:print
// 定义另一个与 Point 类相关的非成员函数,用于打印点的坐标
void print(const Point& p) {
    std::cout << "(" << p.x_ << ", " << p.y_ << ")" << std::endl;
}

#endif  // POINT_H

构造函数再探

  • 某些类不能依赖于合成的默认构造函数 P236
    1. 已有构造函数:如果类已经显式定义了构造函数,编译器将不会自动生成默认构造函数。这种情况下,如果需要使用默认构造函数,必须手动定义。

    2. 成员需要显式初始化:
      • 如果类中有需要显式初始化的成员,例如某些内置类型的变量或复杂类型(如数组、指针等),这些成员无法通过默认构造函数自动初始化,需要用户手动编写构造函数来初始化它们。
      • 若类的成员包含复杂类型或自定义类类型的成员,默认构造函数无法满足初始化要求,因此应自行定义构造函数。
    3. 禁止生成默认构造函数的情境:在一些特殊情况下,例如类中包含指向其他类的引用成员或常量成员,默认构造函数无法自动生成,因为这些成员必须在初始化时提供值。

    4. 防止错误的默认构造:对于一些特定的类,编译器生成的默认构造函数可能导致错误操作,这种情况下最好自行定义构造函数,确保初始化符合需求。

访问控制与封装

友元

友元声明只能在类定义的内部。友元不是类的成员也不受它所在区域访问控制级别的约束。

通过声明友元,可以让其他类或者其他类的特定成员函数访问自身的所有成员。

  • 令成员函数作为友元:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
      class Screen;  // 前向声明 Screen 类,以便在 Window_mgr 中引用
    
      class Window_mgr {
      public:
          void clear();  // 在 Window_mgr 中声明 clear 函数,但还不能定义
      };
    
      class Screen {
          friend void Window_mgr::clear();  // 将 Window_mgr::clear 声明为 Screen 的友元
    
      //Screen功能
      //······
      };
    
      // 现在我们可以定义 clear 函数了
      void Window_mgr::clear() {
          // 假设我们要将 Screen 对象的内容清除为 ' ' (空格)
          Screen screen(5, 5, 'X');  // 创建一个 Screen 对象用于演示
          screen.contents = std::string(screen.height * screen.width, ' ');  // 直接访问私有成员
          screen.display();  // 显示清除后的 Screen
      }
    
  • 封装的益处

    1. 保护对象状态:封装确保用户代码无法随意破坏对象的内部状态。通过将数据成员设为 private,类的作者可以在不影响用户代码的情况下,随时修改类的实现细节,保证代码的灵活性和安全性。

    2. 简化维护:封装限制用户代码对数据的直接访问,从而减少因代码修改而带来的错误。如果类的数据是 public 的,任何对数据的修改都可能影响到依赖该类的用户代码,增加了维护难度和错误修正的复杂性。而通过 private 访问权限的设置,能够使修改更加安全,不需要频繁地更改用户代码,简化了代码维护。

类的其他特性

返回*this的成员函数

1
2
3
4
5
6
7
8
9
10
11
Screen &Screen::move(pos r, pos c) {
    //······

    return *this;
}

Screen &Screen::set(char ch) {
    //······

    return *this;
}

通过返回 *this 我们就可以实现链式编程:myScreen.move(4, 0).set('#') 。

需要注意的是,如果链式函数中某个函数返回的是常量引用,后续函数就不能有修改操作。

  • 基于const的重载

    非常量版本的函数对于常量对象是不可用的,所以我们可以重载一个常量版本函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
      class Screen {
      public:
          // 根据对象是否是 const 重载了 display 函数
          Screen &display(std::ostream &os)
          { do_display(os); return *this; }
            
          const Screen &display(std::ostream &os) const
          { do_display(os); return *this; }
    
      private:
          // 该函数负责显示 Screen 的内容
          void do_display(std::ostream &os) const { os << contents; }
          // 其他成员与之前的版本一致
      };
    

    调用:

    1
    2
    3
    4
    
      Screen myScreen(5, 3);
      const Screen blank(5, 3);
      myScreen.set('#').display(cout); // 调用非常量版本
      blank.display(cout);             // 调用常量版本
    

总结如下:

  • 对于公共代码使用私有辅助函数
    1. 避免重复代码:使用私有的辅助函数 do_display 可以避免在多个地方重复相同的代码,提高代码的可维护性。
    2. 便于扩展和维护:随着类的复杂度增加,display 函数可能会变得更加复杂。将重复操作放入私有的 do_display 函数中,有助于在一个位置集中实现核心操作,方便日后调整。
    3. 便于调试和日志记录:可以在 do_display 中增加测试和调试信息,不必在多个位置进行更改。删除或修改这些信息也会更加方便。

    4. 其他特性
      • 定义私有函数 do_display 并不会带来运行时开销,因为编译器会内联这些调用。
      • 这种设计模式在C++中很常见,通过将常用的功能封装到私有函数中,便于在类中分组和组织相似的操作。

名字查找与类的作用域

寻找类型名的重命名,遵循的规则是:由内而外进行寻找。

所以以下情况 balance 返回的将是 double 类型。

1
2
3
4
5
6
7
8
9
typedef float Money;
class Account {
typedef double Money;
public:
    Money balance() { return bal; }

private:
    Money bal;
} 

❗类内的 typedef 请定义在类开始的时候,如果成员使用了外层作用域的某个名字,类内后来重新定义改名字,会发生错误。

构造函数再探

隐式的类类型转换

在 Sales_data 类中,接受 string 地构造函数和接受 istream 的构造函数分别定义了从这两种类型向 Sales_data 隐式转换的规则:

1
2
3
4
string null_book = "9-99-999";

//自动创建一个临时的Sales_data对象
item.combine(null_book);

需要注意的是编译器只会自动地执行一步类型转换,所以

1
item.combine("9-99-999")

是会报错的,”9-99-999” 并不是string对象。

抑制构造函数定义的隐式转换

通过 explicit 关键字,禁止构造函数隐式创建类本身的对象。

但是我们可以通过显示构造对象来实现。

1
item.combine(Sales_data(null_book));

聚合类

1
2
3
4
5
6
struct Data {
    int ival;
    string s;
}

Data val1 = { 0, "Anna" };

注意成员都是 public 的。

字面值常量类

  • 数据成员都必须是字面值类型
  • 类必须至少还有一个constexpr构造函数
  • constexpr构造函数必须初始化所有数据成员

类的静态成员

静态数据成员不属于任何一个对象,一般来说,不能在类的内部初始化静态成员,假如提供字面值,则可以在类内初始化静态成员。

存在于程序的整个生命周期中。

第 Ⅱ 部分 C++标准库

第九章 顺序容器

顺序容器操作

不要保存end返回的迭代器

当我们添加/删除容器的元素后,原来end返回的迭代器总是会失效,所以我们绝不要保存尾迭代器的值,用到的时候直接调用。

第十章 泛型算法

C++
c++
本文由作者按照 CC BY 4.0 进行授权
分享

最近更新

  • ROS2中并发连接蓝牙手柄排错过程
  • 自行编译ros2-humble-plotjuggler
  • Ubuntu 22.04上蓝牙无法打开——(MT7922网卡为例)
  • Isaac sim遥操作控制
  • RPC:原理、组成与局限性
外部链接
  • codetime
  •  此博客的 Github 仓库

文章内容

相关文章

2024/12/14

智能指针

为了更容易(同时也更安全)地使用动态内存,新的标准库提供了两种智能指针来管理动态对象。智能指针的行为类似常规指针,主要是自动释放所指向的对象。shared_pt 允许多个指针指向同一个对象;unique_ptr 则独占所指向的对象。 shared_ptr 当新的 shared_ptr 对象与指针关联时,则在其构造函数中,将与此指针关联的引用计数增加1。 当任何 shared_pt...

2024/12/13

C++虚函数的实现基本原理

C++作为面向对象的语言,主要有三大特性:继承、封装、多态,都是为了一句话:面对不同对象时,展现出不同的行为。因此C++的多态分为静态多态(编译时多态)和动态多态(运行时多态)两大类。静态多态通过重载、模板来实现;动态多态就是通过本文的主角虚函数来体现的。 虚函数的内存分布 虚函数是通过一张虚函数表来实现的。在这个表中,用指针存储着虚函数的地址。 1 2 3 4 5 6 7 8 9 10...

2024/12/04

C++八股随记

函数重载 群友聊天看到的题。 1 2 3 4 5 6 7 8 9 10 struct A { A() {}//构造函数 ~A() {} float operator+(const float&amp; x); float operator-(const A&amp; x); int operator()(std::string s, int x,...

GAMES104 07.游戏中渲染管线、后处理和其他的一切

GAMES104 08.游戏引擎的动画技术基础(上)

© 2025 Ycyofmine. 保留部分权利。

本站采用 Jekyll 主题 Chirpy

热门标签

games104 UE c++ robot OS 装机 计网 essay

发现新版本的内容。