way to c++

本文介绍了C++编程中的一些关键概念,包括函数的使用,如重载和赋值运算符;迭代器的应用,如遍历容器和插入元素;指针和内存管理,如unique_ptr的智能指针特性;输入输出操作,如cin和cout的注意事项;以及向量的排序和异常处理机制。此外,还讨论了C++标准库中的函数,如sort和transform,以及如何利用它们优化代码。
摘要由CSDN通过智能技术生成
  • 不要清空缓存,速度会比较慢

endl是 清空缓存+换行,所以不如只用\n快 std::cout << content << "\n";


//c++20标准编译
std::transform(std::begin(s), std::end(s), std::begin(s), std::toupper);
//报错:没有与参数列表匹配的 重载函数 "std::transform"实例
//更改为以下
std::transform(std::begin(s), std::end(s), std::begin(s), ::toupper);
  • 在C++中,单引号表示字符,双引号表示字符串。eg:在定义一个数组的时候 string s = {"ac", "asda"}; 定义的是一个字符串数组,这是字符串元素要用双引号。而单引号包裹的一个字符实际上代表一个整数。

  1. 'A' 表示单个字符大写字母A,占用1个字节空间

  1. "A" 表示字符串,该字符串只有1个大写字母A组成,占用2个字节空间,每个字符串末尾会自动加上1个空字符'\0'

  1. 空字符常量使用转义符号'\0'表示,空白字符串使用双引号表示""

  • i++与++i的区别

  1. a=i++ , a 返回原来的值a=i,i=i+1;

a=++i , a 返回加1后的值,a=i+1,i=i+1。

也就是i++是先赋值,然后再自增;++i是先自增,后赋值。

  1. 第二个区别就是: i++ 不能作为左值,而++i可以

  • 寻找向量中最短和最长的字符串


namespace ranges = std::ranges;
//ranges::sort(words); sort 是按照abcd排序, 排序后再挨个比较长度有点啰嗦
//ranges::minmax_element  这段代码使用<algorithm>header中的minmax_element
//算法来寻找words向量中最短和最长的字符串。传递给 minmax_element 的 lambda 函数
//比较了两个字符串的长度,如果第一个字符串比第二个短,则返回 true。
//auto [min_it, max_it] 语法使用结构化绑定来捕获分别指向最短和最长字符串的迭代器。
auto [min_it, max_it] = ranges::minmax_element(words, [](const auto& a, const auto& b) {
        return a.length() < b.length();
    });
    cout << "The length of the shortest string is " << min_it->length() << "\n";
    cout << "The length of the longest string is " << max_it->length() << "\n";
    return 0;
}
  • 符号→在C++中用来表示一个结构或类的成员。它被称为 "箭头运算符",用于通过指向结构或类的指针来访问该结构或类的成员。


struct Point {
    int x;
    int y;
};

Point p = {1, 2};
Point* ptr = &p;//声明一个名为ptr的指针,它指向一个Point类型的对象,并用p的地址初始化它。
            //ptr可以通过*运算符访问它所指向的对象,也可以通过->运算符访问它所指向对象的成员。

int x = ptr->x;
int y = ptr->y;
//->操作符被用来通过ptr指针访问Point结构的x和y成员。
//这相当于写(*ptr).x和(*ptr).y,但更简洁,更容易阅读。

在之前的代码中,min_it是一个指向输入字符串中最短单词的指针。操作符 → 用于访问 min_it 所指向的字符串对象的 length() 成员函数。这相当于写(*min_it).length(),但更简明,更容易阅读。

min_it 和 max_it 是指针。

min_it和max_it被声明为结构化绑定声明的一部分。ranges::minmax_element 算法返回一对迭代器,指向范围中最短和最长的元素。结构化绑定声明允许我们提取这些迭代器,并以简洁和可读的方式将它们分配给变量 min_it 和 max_it。

由于min_it和max_it是迭代器,它们本质上是指向词向量中元素的指针。我们可以使用*操作符来解除对这些指针的引用,并访问它们所指向的元素。例如,(*min_it).length()会给我们words向量中最短的单词的长度。

  • Report an error by throwing an exception


class Bad_area {}; // a class is a user defined type
// Bad_area is a type to be used as an exception
int area(int length, int width)
{
    if (length <= 0 || width <= 0) throw Bad_area(); // note the ()
    return length * width;
}
//Catch and deal with the error(e.g., in main())
try {
    int z = area(x,y);  // if area doesn't throw an exception
}                       // make the assignment and proceed
catch (Bad_area){       // if area() throws Bad_area(), respond
    cout << "opps! Bad area calculation - fix program\n";
} 
  • 向量是一种可以动态调整大小的数组,它可以存储任意类型的数据。这个函数有两个参数:一个是输入流的引用(istream& in),另一个是向量的引用(vector<double>& hw)。引用是一种特殊的变量,它可以让函数直接修改传递给它的参数,而不是创建一个副本。这样可以提高效率和减少内存消耗。

这个函数的返回值也是输入流的引用,这样可以让函数被连续调用,例如read_hw(cin, hw1) >> read_hw(cin, hw2)。这个函数的实现如下:


istream& read_hw(istream& in, vector<double>& hw)
{
    if (in) { // 如果输入流是有效的,即没有发生错误或到达文件末尾
        hw.clear();   // 清空向量中原有的数据

        // 读取作业成绩
        double x; // 定义一个临时变量用来存储输入的数据
        while (in >> x) // 当输入流中还有数据时,将数据读入x,并判断是否成功
            hw.push_back(x); // 将x添加到向量的末尾

        // 清除输入流中的错误标志,以便下一次读取
        in.clear();
    }
    return in; // 返回输入流
}

这个函数的作用是从一个输入流中读取一个学生的信息,包括姓名、期中考试成绩、期末考试成绩和作业成绩。这个函数有两个参数:一个是输入流的引用(istream& in),另一个是一个自定义类型的引用(student_info& s)这个函数的返回值也是输入流的引用,同样可以让函数被连续调用。这个函数的实现如下:


istream& read(istream& in, student_info& s)
{
    // 从输入流中读取学生的姓名、期中考试成绩和期末考试成绩
    in >> s.name >> s.midterm >> s.final;
    // 调用read_hw函数,从输入流中读取学生的作业成绩,并存储在s.homework中
    return read_hw(in, s.homework);
}

student_info是一个结构体(struct),它可以将多个相关的数据组合在一起,例如:


struct student_info
{
    string name;              // students name
    double midterm, final;    // midterm and final exam grades
    vector<double> homework;  // all homework grades
};

如何排序?


std::vector<student_info> students;
std::ranges::sort(students);
//sort() uses the operator < to determine order
//Makes no sense for student_info’s!
  1. We can teach sort() how to order by specifying a predicate:


// compare two student_info instances, return whether 'x‘
// is smaller than 'y' based on comparing the stored names
// of the students
bool compare(student_info const& x, student_info const& y)
{
    return x.name < y.name;
}
// Now, we can use this function as:
std::vector<student_info> students;
std::ranges::sort(students, compare);
  1. Alternatively, we could define an appropriate operator:


bool operator<(student_info const& x, student_info const& y)
{
    return x.name < y.name;
}
// Now, we can use this function as:
std::vector<student_info> students;
std::ranges::sort(students);
  1. Alternative: lambda function:


// sorting the student data using a lambda function
std::ranges::sort(students, 
    [](student_info const& x, student_info const& y)
    {
        return x.name < y.name;
    }
);
//Much nicer! Everything is in one place
  • 在创建函数时,必须编写其定义。所有函数定义包括以下组成部分:

  1. 名称:每个函数都必须有一个名称。通常,适用于变量名称的规则同样也适用于函数名称。

  1. 形参列表:调用函数的程序模块可以向其发送数据。形参列表是保存传递给函数的值的变量列表。如果没有值传递给函数,则其形参列表为空。

  1. 主体:函数的主体是处理函数正在执行的任务的一组语句。这些语句包含在一组大括号中。

  1. 返回类型:函数可以将值发送回调用它的程序模块。返回类型是要发送回的值的数据类型。

  • void 的字面意思是“无类型”,它通常用于以下三种情况下:

  • 函数返回为空,表示函数不返回任何值。例如,void exit(int status);

  • 函数参数为空,表示函数不接受任何参数。例如,int rand(void);

  • 指针指向 void,表示指针可以指向任何类型的数据。例如,void *ptr;

void 类型不能用来定义变量,也不能用来做类型转换。如果要将 void 类型的指针转换为其他类型的指针,需要使用强制类型转换运算符。例如:


void *ptr; // 定义一个 void 类型的指针
int *p = (int *)ptr; // 将 void 类型的指针转换为 int 类型的指针
  • while (true) 在 C++ 中也一种循环结构,它表示当 while 后的条件为真时执行循环体,而 true 是一个布尔类型的常量,它总是为真,所以 while (true) 表示无限循环,也就是永远不会停止的循环。

通常,在 while (true) 循环中会加入 break 语句或其他条件判断来在满足某个条件时终止循环。例如:


int i = 0;
while (true) { // 无限循环
  cout << i << endl;
  i++;
  if (i == 10) { // 判断 i 是否等于 10break; // 终止循环
  }
}
  • cpp和hpp的区别:

  • cpp 文件一般用来写类或函数的实现代码,也就是定义类的成员函数或者普通函数的具体逻辑。cpp 文件需要被编译器编译成 obj 文件,然后链接成可执行文件或库文件。

  • hpp 文件一般用来写类或函数的声明代码,也就是定义类的成员变量或者函数原型,以及一些常量或宏等。hpp 文件不需要被编译器编译,而是被其他 cpp 文件通过 #include 指令包含进来,相当于复制粘贴 hpp 文件的内容到 cpp 文件中。

  • cpp 和 hpp 文件一般是配对的,也就是说一个 cpp 文件对应一个同名的 hpp 文件,这样可以方便地组织和管理代码。在 cpp 文件中,一般要 #include 对应的 hpp 文件,以及其他需要用到的 hpp 文件。

  • hpp 文件有时候也会包含一些实现代码,比如模板类或函数,内联函数等,这是因为这些代码需要在编译时就能看到实现,而不能在链接时才找到实现。这样的 hpp 文件相当于一个完整的模块,只需要被 #include 就可以使用,不需要再编译或链接。

  • cin 是 C++ 中的标准输入流对象,它可以作为函数的参数传递,但是有一些注意事项:

  • cin 作为函数的参数时,应该使用引用类型,即 istream& 类型,这样可以避免拷贝开销和保持输入流的状态。

  • cin 作为函数的参数时,应该使用 const 修饰符,即 const istream& 类型,这样可以避免在函数内部修改输入流的状态或内容。

  • cin 作为函数的参数时,应该在函数内部检查输入流的状态,即使用 cin.good() 或 cin.fail() 等方法判断是否有错误发生,如果有错误,应该及时处理或返回。

  • cin 作为函数的参数时,应该在函数内部清空输入流的缓冲区,即使用 cin.ignore() 或 cin.clear() 等方法清除多余的字符或错误标志,以免影响后续的输入操作。

一个简单的从输入流读取一个整数的函数可以定义如下:


int readInt(const istream& in) {
  int i;
  in >> i; // 从输入流读取一个整数
  if (in.fail()) { // 检查是否有错误发生
    cout << "Invalid input!" << endl; // 输出错误信息
    return -1; // 返回一个特殊值表示错误
  }
  in.ignore(1000, '\n'); // 清空输入流缓冲区直到换行符
  return i; // 返回读取到的整数
}
  • 应该使用#include “read_words.hpp”,而不是#include “read_words.cpp”。因为read_words.hpp是头文件,它包含了函数的声明,而read_words.cpp是源文件,它包含了函数的定义。如果使用#include “read_words.cpp”,可能会遇到重复定义的错误,因为编译器会看到两个相同的函数定义。头文件的作用是让其他文件知道函数的接口,而不是实现。


// task1.cpp
// count the number of words in the input

#include <iostream>
#include <string>
#include <vector>
#include "read_words.hpp"

using std::string;   using std::vector;
using std::istream;  using std::cin;
using std::cout;     

int main()
{
    vector<string> words;
    read(cin, words);
    cout << words.size() << "\n";
    return 0;
}

// read_words.cpp
//Write a function that reads words from an input stream and stores
//them in a vector.

#include "read_words.hpp"

void read(istream& in, vector<string>& words)
{
    if (in)
    {
        words.clear();
        string s;
        while (in >> s && s != "quit") //CTRL+Z 也可以结束输入
        {
            words.push_back(s);
        }
        in.clear();
    }
}

// read_words.hpp
#ifndef GUARD_READ_WORDS_HPP
#define GUARD_READ_WORDS_HPP

#include <iostream>
#include <string>
#include <vector>

using std::istream;     using std::vector;
using std::string;

void read(istream& in, vector<string>& words);

#endif

不能只编译task1.cpp文件,还需要把read_words.cpp文件一起编译和链接,否则程序会找不到read_words函数的定义。一个简单的方法是把所有的.cpp文件都放在同一个命令行里:

终端:g++ -o -std=c++20 task1 task1.cpp read_words.cpp (看看文件是在对应目录下吗?)

终端:task1.exe (如果是在当前文件夹路径下的,可直接运行,不在的话输入它的绝对路径)

  • 如果想用b的元素替换a的某个元素,需要使用vector::erase ()函数来删除那个元素。例如,如果想用b替换a的第三个元素,可以这样做:


// 先删除a的第三个元素
auto pos = a.erase (a.begin () + 2);
// 再在同样的位置插入b
a.insert (pos, b.begin (), b.end ());

//注意这里pos为被删掉的那个元素的指针,如果想删除一个元素,返回原向量
a.erse(a.begin() + 2);
return a;

可以使用std::span来表示一个向量的切片。std::span是C++20中引入的一个类模板,它可以表示一个连续的元素序列,而不需要拷贝原始数据。


#include <span>
// 假设你想把向量b从第三个元素到最后一个插入到向量a
std::span b_slice(b.begin () + 2, b.end ()); // 创建一个b的切片
a.insert (a.end (), b_slice.begin (), b_slice.end ()); // 把切片插入到a的末尾
//可以插入到a的各个位置,改变第一个参数即可
  • 如果在输入流中也要考虑空格,那么getline()函数是一个很好的选择,因为它可以读取整行,包括空格。而cin会忽略空格,只读取第一个单词。在C++中,默认的分隔符是换行符,也就是’\n’。如果想要使用其他的分隔符,可以在getline()函数中指定第三个参数。


std::string s;
std::getline(std::cin, s, ','); // Use ',' as the delimiter
  • 检查包含元素类型


std::string s;
//只有数字,返回true
std::all_of(s.begin(), s.end(), ::isdigit);
//有数字,返回true
std::any_of(s.begin(), s.end(), ::isdigit);
//没有非数字
std::none_of(s.begin(), s.end(), [](char c) {return !isdigit(c);});

不只能检查string,还可以检查其他类型的容器,只要它们支持迭代器。std::all_of函数的参数是一个范围[first, last),其中first和last是迭代器,可以指向任何类型的元素。只需要提供一个合适的一元谓词,来判断每个元素是否满足条件。例如,可以检查一个vector<int>是否都是偶数:


std::vector<int> v = {2, 4, 6, 8};
bool all_even = std::all_of(v.begin(), v.end(), [](int n) {return n % 2 == 0; });
std::cout << std::boolalpha << all_even << "\n"; 
// Output: true

//自定义检查数据,在给定范围里有没有不等于[]里的line_number
if (all_of(ret[*it].begin(), ret[*it].end(), [line_number](int c) {return c != line_number;})) {
    ret[*it].push_back(line_number);
}
  • std::unique_ptr是一个智能指针,它通过一个指针拥有并管理另一个对象,并在unique_ptr离开作用域时释放该对象。

unique_ptr对象通过关联的删除器来释放对象,当以下情况之一发生时:

  • 管理unique_ptr对象被销毁

  • 管理unique_ptr对象通过赋值操作或reset方法被赋予另一个指针

unique_ptr对象独占地拥有它们的指针:没有其他设施负责删除该对象,因此没有其他管理指针应该指向它们管理的对象,因为一旦它们必须这样做,unique_ptr对象就会删除它们管理的对象,而不考虑是否还有其他指针指向同一个对象,从而使任何其他指向那里的指针变为指向无效位置。

unique_ptr对象有两个组成部分:

  • 一个存储的指针:指向它管理的对象的指针。这在构造时设置,可以通过赋值操作或调用成员reset来改变,也可以使用成员get或release来单独访问。

  • 一个存储的删除器:一个可调用的对象,它接受一个与存储的指针相同类型的参数,并被调用来删除管理的对象。它在构造时设置,可以通过赋值操作来改变,也可以使用成员get_deleter来单独访问。

unique_ptr对象通过运算符*和->(对于单个对象)或运算符[](对于数组对象)来提供对其管理对象的访问。出于安全原因,它们不支持指针算术,并且只支持移动赋值(禁用复制赋值)。

  • a.begin()和begin(a)的区别是:

a.begin()是一个成员函数,它只能用于有begin()成员函数的容器类型,如vector, list, map等。

begin(a)是一个非成员函数,它可以用于任何可以迭代的类型,如容器,数组,初始化列表等。

a.begin()和begin(a)通常返回相同的迭代器,但是如果a是一个const容器,那么a.begin()返回一个const_iterator,而begin(a)返回一个iterator。

begin(a)可以自动推断a的类型,而a.begin()需要显式指定a的类型。

所以,在一般情况下,建议使用begin(a),因为它更通用,更简洁,更灵活。

===========================================================

a.end()是一个成员函数,它返回一个指向容器末尾(即最后一个元素之后)的迭代器。如果a是一个标准容器,那么当a不是const限定时,它返回一个C::iterator类型的迭代器,否则返回一个C::const_iterator类型的迭代器。

end(a)是一个非成员函数,它也返回一个指向容器末尾的迭代器。但是,它可以接受任何类型的参数,只要该类型支持a.end()或者有合适的重载函数。例如,end(a)可以接受一个数组作为参数,并返回一个指向数组末尾的指针。

  • 迭代器适配器

我的理解:先将要复制的元素赋值给迭代器适配器生成的迭代器,然后再将迭代器适配器生成的迭代器所指向的元素放到目的地,以此来达到将元素复制和在容器末尾插入元素的概念分离开来。

查阅:它是通过重载赋值运算符来实现的。迭代器适配器生成的迭代器是一个类模板,它有一个成员变量,用来保存目标容器的引用或者指针。当我们给这个迭代器赋值一个元素时,它会调用目标容器的相应成员方法,将元素插入到容器中。例如,back_insert_iterator 会调用 push_back() 方法,front_insert_iterator 会调用 push_front() 方法,insert_iterator 会调用 insert() 方法。这样,我们就可以使用赋值运算符来实现复制的功能,而不需要直接操作容器。

  • 关于重载

重载是指在同一作用域中,给一个函数或者运算符指定多个定义,以便根据不同的参数类型或者个数,选择合适的函数或者运算符来执行。重载函数就是指具有相同名称,但是参数列表不同的函数。例如,我们可以定义两个名为 max 的函数,一个用来比较两个整数,一个用来比较两个浮点数:


// 比较两个整数的 max 函数
int max(int a, int b) {
  return a > b ? a : b;
}

// 比较两个浮点数的 max 函数
double max(double a, double b) {
  return a > b ? a : b;
}
//这样,当我们调用 max 函数时,编译器会根据参数的类型,选择合适的函数来执行。例如:
int x = 10;
int y = 20;
double m = 3.14;
double n = 2.71;

cout << max(x, y) << endl; // 调用 int 类型的 max 函数
cout << max(m, n) << endl; // 调用 double 类型的 max 函数

重载运算符是指给一个已有的运算符指定新的功能,以便根据不同的操作数类型或者个数,选择合适的运算符来执行。重载运算符的本质是一个函数,它的名称是由关键字 operator 和要重载的运算符符号构成的。例如,我们可以定义一个名为 Complex 的类,用来表示复数,并且重载 + 运算符,使得可以对两个复数进行加法运算:


// 定义一个表示复数的类
class Complex {
public:
  // 带有参数的构造函数,接受double类型的值,并使用初始化列表来初始化类的数据成员real和imag
  Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {}//
  
  // 返回实部
  double getReal() const {
    return real;
  }
  
  // 返回虚部
  double getImag() const {
    return imag;
  }
  
  // 重载 + 运算符,用于两个复数相加
  Complex operator+(const Complex& c) const {
    // 返回一个新的复数对象,其实部和虚部分别为两个复数的实部和虚部之和
    return Complex(real + c.real, imag + c.imag);
  }
  
private:
  // 实部和虚部
  double real;
  double imag;
};
//这样,当我们使用 + 运算符对两个复数对象进行加法运算时,编译器会调用重载的 + 函数来执行。例如:
Complex c1(1.0, 2.0); // 创建一个复数对象 c1,其实部为 1.0,虚部为 2.0
Complex c2(3.0, 4.0); // 创建一个复数对象 c2,其实部为 3.0,虚部为 4.0

Complex c3 = c1 + c2; // 使用重载的 + 运算符对 c1 和 c2 进行加法运算,得到一个新的复数对象 c3

cout << "c3 = " << c3.getReal() << " + " << c3.getImag() << "i" << endl; // 输出 c3 的实部和虚部
//输出结果是:
c3 = 4 + 6i

重载赋值运算符是指给赋值运算符( = )指定新的功能,以便根据不同的操作数类型或者个数,选择合适的赋值运算符来执行。重载赋值运算符的本质是一个成员函数,它的名称是 operator=,它的参数是要赋值的对象的引用。例如,我们可以定义一个名为 Distance 的类,用来表示距离,并且重载赋值运算符,使得可以对两个 Distance 对象进行赋值操作:


// 定义一个表示距离的类
class Distance {
public:
  // 构造函数
  Distance(int f = 0, int i = 0) : feet(f), inch(i) {}
  
  // 返回英尺
  int getFeet() const {
    return feet;
  }
  
  // 返回英寸
  int getInch() const {
    return inch;
  }
  
  // 重载赋值运算符,用于两个 Distance 对象之间的赋值
  Distance& operator=(const Distance& d) {
    // 如果两个对象不是同一个对象,则进行赋值操作
    if (this != &d) {
      feet = d.feet; // 将右侧对象的英尺赋值给左侧对象的英尺
      inch = d.inch; // 将右侧对象的英寸赋值给左侧对象的英寸
    }
    // 返回左侧对象的引用
    return *this;
  }
  
private:
  // 英尺和英寸
  int feet;
  int inch;
};
//这样,当我们使用赋值运算符对两个 Distance 对象进行赋值操作时,
//编译器会调用重载的 operator= 函数来执行。例如:
Distance d1(10, 5); // 创建一个 Distance 对象 d1,其英尺为 10,英寸为 5
Distance d2(8, 3); // 创建一个 Distance 对象 d2,其英尺为 8,英寸为 3

d1 = d2; // 使用重载的赋值运算符对 d1 和 d2 进行赋值操作
// 输出 d1 的英尺和英寸
cout << "d1 = " << d1.getFeet() << " feet " << d1.getInch() << " inch" << endl;
// 输出 d2 的英尺和英寸
cout << "d2 = " << d2.getFeet() << " feet " << d2.getInch() << " inch" << endl;
// 输出结果是:
d1 = 8 feet 3 inch
d2 = 8 feet 3 inch
  • accumulate函数是C++ STL中numeric头文件中的一个函数,它可以对一个范围内的元素进行累加操作,也可以接受一个二元操作符作为参数,来指定如何累加元素。

accumulate函数的语法有两种形式:

accumulate (start, end, initial_sum); 这里,start是迭代器的初始位置,end是迭代器的最后一个位置,initial_sum是累加的初始值。这种形式的accumulate函数默认使用加法来累加元素。

accumulate (start, end, initial_sum, func); 这里,func是一个二元操作符,它指定了如何累加元素。例如,我们可以使用乘法或者减法来累加元素。

下面是一些accumulate函数的示例:


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

int main()
{
    // 初始化一个数组
    int a[] = {1, 2, 3, 4, 5};

    // 使用默认的加法来累加数组中的元素
    int sum = accumulate(a, a + 5, 0);
    cout << "sum = " << sum << endl; // 输出 sum = 15

    // 使用乘法来累加数组中的元素
    int product = accumulate(a, a + 5, 1, [](int x, int y) {
        return x * y;
    });
    cout << "product = " << product << endl; // 输出 product = 120

    // 使用减法来累加数组中的元素
    int difference = accumulate(a, a + 5, 0, std::minus<int>());
    cout << "difference = " << difference << endl; // 输出 difference = -15

    return 0;
}

string::size_type width(const vector<string>& v)
{
    string::size_type maxlen = 0;
    for (vector<string>::size_type i = 0; i != v.size(); ++i)
        maxlen = max(maxlen, v[i].size());
    return maxlen;
}
------------------------------------------------------------
//rewrit
string::size_type width(const vector<string>& v)
{
    return accumulate(v.begin(), v.end(), 0, [](string::size_type maxlen, const string& s) {
        return max(maxlen, s.size());
    });
}
  • size_t 是一种无符号整数类型,用于表示大小或长度。它实际上是一个与系统相关的类型,在32位系统上它通常是unsigned int,在64位系统上它通常是unsigned long int。我在这里使用size_t来表示学生人数,因为人数是一个大于0的整数,使用无符号类型更合适。如果使用signed int,那么当人数超过INT_MAX的时候,就会发生整数溢出,导致错误的结果。所以,简而言之,size_t是一种无符号的整数类型,用于表示非负的大小或长度的值。在这里,我使用它来安全地表示学生人数,而不必担心整数溢出的问题。另外,和人数相关的计数(A,B,C,D和F)也使用了size_t类型,以保持一致性。总之,当你需要表示非负的计数或大小时,size_t是一种很好的选择,可以避免整数溢出带来的问题。

  • 在C++中,左值(lvalue)和右值(rvalue)是两个重要的概念。

  • 左值:

- 左值是那些具有确定地址的表达式。这些表达式的值可以被修改。

- 左值可以出现在赋值语句的左侧和右侧。

- 典型的左值有:变量、函数调用、数组元素等。

  • 右值:

- 右值是那些没有确定地址的临时表达式。这些表达式的值不能被修改。

- 右值只能出现在赋值语句的右侧。

- 典型的右值有:字面量、表达式、函数调用后的返回值等。例子:


    //cpp
    int x = 1;     // x 是左值,1 是右值
    
    int y = x;      // x 是左值,可以赋值给 y
    x = y;          // x 和 y 都是左值,可以互相赋值
    
    x = x + 1;      // x 是左值,x + 1 是右值,可以赋值给左值 x
    
    1 = 2;          // 错误,1 是右值,不能赋值给其他表达式

C++11中引入了右值引用的概念,用于绑定右值,避免不必要的临时对象产生和拷贝操作。利用右值引用和移动构造函数可以实现移动语义,提高性能。所以在C++中,区分左值和右值是很重要的,它们涉及表达式的分类、赋值语句的可行性、移动语义等许多内容。

  • 在C++中,\t表示水平制表符(horizontal tab)。它可以用于在输出中对齐文本。


//cpp
#include <iostream>

int main() {
    std::cout << "Name\tAge\tGender" << std::endl;
    std::cout << "Jack\t20\tMale" << std::endl;
    std::cout << "Jill\t22\tFemale" << std::endl;
}
//out
Name    Age   Gender 
Jack   20     Male   
Jill   22     Female

可以看到,\t对第一列(Name)之后的输出进行了空格填充,以实现对齐的效果。在代码中,\t实际上会被替换成一定数量的空格,以实现对齐。这个数量通常是8,但在不同环境中可能会不同。所以总结一下,\t在C++中用于在输出中插入水平制表符,目的是对齐文本和格式化输出。

  • rbegin()指向容器末尾的下一个位置,但是对rbegin()解引用却指向的是容器末尾的元素。

这是因为反向迭代器的工作原理是先递减,再解引用。也就是说,如果你有一个反向迭代器r,那么r的值其实是(r-1)的值。这样做的目的是为了方便地从后往前遍历容器,而不需要考虑容器的边界。

  • 为什么要在函数后加const呢?

  • 为了保证数据的安全性。如果一个对象是const的,那么它只能调用常成员函数,这样可以避免意外地改变对象的状态。例如:


class String {
public:
    // ...
    char& operator[](int index); // 非常成员函数,可以修改字符串
    const char& operator[](int index) const; // 常成员函数,只能访问字符串
    // ...
};

//第一个[]运算符是一个非常成员函数,它接受一个int类型的索引,返回一个char类型的引用。
//这个引用可以作为左值或右值使用,也就是说,可以用它来修改或访问字符串中的字符。
//第二个[]运算符是一个常成员函数,它也接受一个int类型的索引,但返回一个const char类型的引用。
//这个引用只能作为右值使用,也就是说,只能用它来访问字符串中的字符,不能用它来修改。
//这个函数后面加了const关键字,表示它不会改变对象的状态,所以它可以被const对象调用。
//可以通过查看函数的声明来判断它是常成员函数还是非常成员函数。
//如果函数后面有const关键字,那么它就是常成员函数,否则就是非常成员函数。
//也可以通过对象是否是const来判断它会调用哪个版本的[]运算符。
//如果对象是const的,那么它只能调用常成员函数,否则只能调用非常成员函数。例如:
const String str("hello"); // 声明一个const字符串
str[0] = 'H'; // 错误,不能调用非常成员函数
char c = str[0]; // 正确,调用常成员函数

String s("hello"); // 声明一个非const字符串
s[0] = 'H'; // 正确,调用非常成员函数
char c = s[0]; // 正确,调用非常成员函数
  • 为了提高代码的可读性。如果一个成员函数后面加了const,那么就很明确地告诉读者这个函数不会改变对象的状态,这样可以方便理解和维护代码。

  • 为了支持重载和多态。有时候我们需要根据对象是否是const来提供不同的行为,这时候就可以利用常成员函数和非常成员函数的重载来实现。例如:


class A {
public:
    // ...
    void print() const; // 常成员函数,打印对象的信息
    void print(); // 非常成员函数,打印对象的信息,并记录日志
    // ...
};

const A a; // 声明一个const对象
a.print(); // 调用常成员函数

A b; // 声明一个非const对象
b.print(); // 调用非常成员函数
  • 一个指向常量的指针或者一个常量的引用


A a1, a2; // 声明两个非const对象
const A *p = &a1; // 声明一个指向常量A的指针,初始化为指向a1
p->f(); // 错误,不能调用非常成员函数
p = &a2; // 正确,可以改变p指向的对象
//p是一个指向常量A的指针,也就是说,p可以指向不同的A对象,但是不能通过p来修改A对象的状态。

A a1, a2; // 声明两个非const对象
const A &q = a1; // 声明一个对常量A的引用,初始化为绑定到a1
q.f(); // 错误,不能调用非常成员函数
q = a2; // 错误,不能改变q绑定的对象
//q是一个对常量A的引用,也就是说,
//q必须绑定到一个A对象上,不能重新绑定,也不能通过q来修改A对象的状态。
  • 指针


 char greeting[] = "Hello";  //[] 声明数组时不指定大小
 char* p1 = greeting;                // 指针变量,指向字符数组变量
//p1是一个指向char类型的指针变量,它可以指向不同的char对象,
//也可以通过p1来修改或访问char对象的值。
    ***
    char* p1 = greeting; // 声明一个指向char类型的指针变量,初始化为指向greeting
    p1[0] = 'h'; // 通过p1修改greeting的第一个元素
    char c = p1[0]; // 通过p1访问greeting的第一个元素
    p1 = "world"; // 改变p1指向的对象
    ***
 const char* p2 = greeting;          // 指针变量,指向字符数组常量
//p2是一个指向const char类型的指针变量,
//它可以指向不同的const char对象,但是不能通过p2来修改char对象的值,只能访问。
    ***
    const char* p2 = greeting; // 声明一个指向const char类型的指针变量,初始化为指向greeting
    p2[0] = 'h'; // 错误,不能通过p2修改greeting的第一个元素
    char c = p2[0]; // 正确,通过p2访问greeting的第一个元素
    p2 = "world"; // 正确,改变p2指向的对象
    ***
 char* const p3 = greeting;          // 常指针,指向字符数组变量
//p3是一个常指针,它必须初始化为指向一个char对象,并且不能改变它所指向的对象。
//但是可以通过p3来修改或访问char对象的值。
    ***
    char* const p3 = greeting; // 声明一个常指针,初始化为指向greeting
    p3[0] = 'h'; // 正确,通过p3修改greeting的第一个元素
    char c = p3[0]; // 正确,通过p3访问greeting的第一个元素
    p3 = "world"; // 错误,不能改变p3指向的对象
    ***
 const char* const p4 = greeting;    // 常指针,指向字符数组常量
//p4是一个常指针,它必须初始化为指向一个const char对象,并且不能改变它所指向的对象。
//也不能通过p4来修改char对象的值,只能访问。
    ***
    const char* const p4 = greeting; // 声明一个常指针,初始化为指向greeting
    p4[0] = 'h'; // 错误,不能通过p4修改greeting的第一个元素
    char c = p4[0]; // 正确,通过p4访问greeting的第一个元素
    p4 = "world"; // 错误,不能改变p4指向的对象
    ***
  • vector是C++标准库中的一个动态数组容器,它可以根据需要自动调整大小和容量。vector有两个重要的属性:size和capacity。size表示vector中实际存储的元素个数,capacity表示vector分配的内存空间可以容纳的元素个数。当size等于capacity时,如果再向vector中添加元素,就会触发扩容操作,即重新分配一块更大的内存空间,并将原来的元素复制到新的空间中。这样做会消耗一定的时间和资源,而且可能导致原来的迭代器失效。

    为了避免频繁的扩容操作,vector提供了两个方法:resize和reserve。它们的区别和作用如下:

    • resize(n):改变vector的大小为n,即改变size的值为n。如果n小于原来的size,则删除多余的元素;如果n大于原来的size,则在末尾添加采用值初始化的元素。resize(n)会影响vector中元素的个数和值,但不一定会影响capacity。
    • reserve(n):改变vector的容量为n,即改变capacity的值为n。如果n小于或等于原来的capacity,则不做任何操作;如果n大于原来的capacity,则重新分配内存空间,但不改变size和元素的值。reserve(n)只是预留空间,不会创建或删除元素,所以不会影响vector中元素的个数和值。
    • 简单来说,resize是改变vector中有效数据的大小,而reserve是改变vector分配内存空间的大小。它们都可以提高vector的性能,但使用时要注意参数的合理性和边界条件。

        

        为避免重复扩容做的机制是预留空间,即在vector创建或添加元素时,多分配一些内存空间,以备后用。这样,当vector中的元素增加时,就不需要每次都重新分配内存空间和复制元素,从而提高效率和性能。预留空间的方法是使用vector的reserve(n)函数,其中n是期望的容量大小。reserve(n)只会改变vector的capacity,不会改变vector的size和元素的值。

        例如,如果我们创建一个空的vector<int> v,并调用v.reserve(10),那么v的capacity就会变为10,但v的size仍然为0,v中也没有任何元素。如果我们向v中添加5个元素,那么v的size就会变为5,但v的capacity仍然为10,不需要扩容。如果我们继续向v中添加6个元素,那么v的size就会变为11,超过了原来的capacity,此时就会触发扩容操作,重新分配更大的内存空间,并将原来的元素复制过去。

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

int main()
{
    vector<int> v; // 创建一个空的vector
    cout << "size: " << v.size() << endl; // 输出size
    cout << "capacity: " << v.capacity() << endl; // 输出capacity
    v.reserve(10); // 预留10个空间
    cout << "size: " << v.size() << endl; // 输出size
    cout << "capacity: " << v.capacity() << endl; // 输出capacity
    for (int i = 0; i < 5; i++)
    {
        v.push_back(i); // 向v中添加5个元素
    }
    cout << "size: " << v.size() << endl; // 输出size
    cout << "capacity: " << v.capacity() << endl; // 输出capacity
    for (int i = 0; i < 6; i++)
    {
        v.push_back(i + 5); // 向v中添加6个元素
    }
    cout << "size: " << v.size() << endl; // 输出size
    cout << "capacity: " << v.capacity() << endl; // 输出capacity
    return 0;
}

// size: 0
// capacity: 0
// size: 0
// capacity: 10
// size: 5
// capacity: 10
// size: 11
// capacity: 20

  • C++空类是指没有任何非静态数据成员、虚函数和虚基类的类。例如:

    class Empty {}; // 空类
    

            C++空类的大小不为0,而是为1。这是因为C++标准规定,每个独立的对象都必须具有非零大小,并且不同的对象不能具有相同的地址。如果空类的大小为0,那么创建多个空类对象时,它们就会占用相同的内存空间,导致无法区分。因此,编译器会给空类分配一个字节的空间,以保证每个对象都有唯一的地址。

  • 在C++中,一个类有八个默认函数,它们是:

    • 默认构造函数:没有参数的构造函数,用于创建对象的默认初始化。如果没有显式定义,编译器会自动生成一个默认的构造函数,什么都不做。
    • 默认拷贝构造函数:用一个同类对象的引用作为参数的构造函数,用于创建对象的拷贝初始化。如果没有显式定义,编译器会自动生成一个默认的拷贝构造函数,逐一拷贝类的数据成员。
    • 默认析构函数:在类名前加上字符~的成员函数,没有参数和返回值,用于在对象销毁时做一些清理工作。如果没有显式定义,编译器会自动生成一个默认的析构函数,什么都不做。
    • 默认重载赋值运算符函数:用一个同类对象的引用作为参数的成员函数,返回类对象的引用,用于实现对象之间的赋值操作。如果没有显式定义,编译器会自动生成一个默认的赋值运算符重载函数,逐一赋值类的数据成员。
    • 默认重载取址运算符函数:没有参数的成员函数,返回类对象指针,用于实现取对象地址的操作。如果没有显式定义,编译器会自动生成一个默认的取址运算符重载函数,返回对象本身的地址。
    • 默认重载取址运算符const函数:没有参数的const成员函数,返回类对象常量指针,用于实现取常量对象地址的操作。如果没有显式定义,编译器会自动生成一个默认的取址运算符重载const函数,返回对象本身的地址。
    • 默认移动构造函数(C++11):用一个同类对象的右值引用作为参数的构造函数,用于创建对象的移动初始化。如果没有显式定义,编译器会自动生成一个默认的移动构造函数(如果满足一定条件),将类的数据成员从右值对象移动到新创建的对象中。
    • 默认重载移动赋值操作符(C++11):用一个同类对象的右值引用作为参数的成员函数,返回类对象的引用,用于实现对象之间的移动赋值操作。如果没有显式定义,编译器会自动生成一个默认的移动赋值操作符(如果满足一定条件),将类的数据成员从右值对象移动到已有对象中。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值