C++ 入门(八)— 常量和字符串

常量变量

在编程中,常量是在程序执行期间不得更改的值。

在C++ 中,命名常量是与标识符关联的常量值。这些有时也称为符号常量,有时也称为常量。

在 C++ 中定义命名常量有三种方法:

  • 常量变量。
  • 带有替换文本的类似对象的宏。
  • 枚举常量。

常量变量

值不能改变的变量称为常量变量。

声明 const 变量:

const double gravity { 9.8 };  //首选在类型之前使用const
int const sidesInSquare { 4 }; // “east const”风格,可以,但不是首选

必须初始化常量变量

在定义常量变量时,必须对其进行初始化,而且无法通过赋值更改该值。

int main()
{
    const double gravity; //error:const变量必须初始化
    gravity = 9.9;        // error:const变量不能更改

    return 0;
}

但是,常量变量可以从其他变量(包括非常量变量)初始化:

int main()
{
    int age{};
    const int constAge { age }; // 使用非const值初始化const变量
    age = 5;      // age是非const的,所以我们可以改变它的值
    constAge = 6; // error: constAge是const,所以我们不能改变它的值

    return 0;
}

常量函数参数

函数参数可以通过以下关键字成为常量:

#include <iostream>

void printInt(const int x)
{
    std::cout << x << '\n';
}

int main()
{
    printInt(5); // 5 will be used as the initializer for x
    printInt(6); // 6 will be used as the initializer for x

    return 0;
}

这里,函数调用中参数的值将用作 的初始值设定项。

注意:按值传递时不要使用,因为值传递 const 对象通常没有多大意义,因为它们是无论如何都会被销毁的临时副本。

常量返回值

函数的返回值也可以设为 const:

const int getValue()
{
    return 5;
}

注意:按值返回时不要使用,因为按值返回 const 对象通常没有多大意义,因为它们是无论如何都会被销毁的临时副本。

首选常量变量而不是预处理器宏

有三个主要问题:

  • 最大的问题是宏不遵循正常的 C++ 范围规则。#defined 宏后,当前文件中所有后续出现的宏名称都将被替换。
    #include <iostream>
    
    void someFcn()
    {
    #define gravity 9.8
    }
    
    void printGravity(double gravity) // 包括这个,导致编译错误
    {
      std::cout << "gravity: " << gravity << '\n';
    }
    
    int main()
    {
      printGravity(3.71);
    
        return 0;
    }
    
    在这里插入图片描述
  • 其次,使用宏调试代码通常更难。尽管源代码将具有宏的名称,但编译器和调试器永远不会看到宏,因为它在运行之前已被替换。
  • 第三,宏替换的行为与 C++ 中的其他所有内容不同。因此,很容易犯不经意的错误。

常量表达式

一种始终可以在编译时计算的表达式称为“常量表达式”。

常量表达式的精确定义很复杂,因此我们将采用简化的观点:常量表达式是仅包含编译时常量和支持编译时计算的运算符/函数的表达式。

编译时常量是一个常量,其值必须在编译时已知。这包括:

  • 文字(例如“5”、“1.2”)。
  • Constexpr 变量。
  • 具有常量表达式初始值设定项的常量积分变量(例如 )。在现代C++中,constexpr变量是首选。
  • 非类型模板参数。
  • 枚举器。

不是编译时常量的常量变量有时称为运行时常量。运行时常量不能在常量表达式中使用。

编译时优化

表达式的编译时求值

比如:const int x { 3 + 4 }; 改成 const int x { 7 };

常量变量更易于优化

因为现在是 const,所以编译器现在有一个保证,在初始化后无法更改。这使得编译器更容易理解它可以安全地从这个程序中进行优化。

根据编译器能够优化变量的可能性对变量进行排名:

  • 编译时常量变量(始终符合优化条件)
  • 运行时常量变量
  • 非常量变量(可能仅在简单情况下进行优化)

Constexpr 变量

constexpr(“常量表达式”的缩写)变量必须使用常量表达式进行初始化,否则将导致编译错误。

#include <iostream>

int five()
{
    return 5;
}

int main()
{
    constexpr double gravity { 9.8 }; // ok: 9.8 is a constant expression
    constexpr int sum { 4 + 5 };      // ok: 4 + 5 is a constant expression
    constexpr int something { sum };  // ok: sum is a constant expression

    std::cout << "Enter your age: ";
    int age{};
    std::cin >> age;

    constexpr int myAge { age };      // compile error: age is not a constant expression
    constexpr int f { five() };       // compile error: return value of five() is not a constant expression

    return 0;
}

常量与 constexpr

  • 对于变量,const 表示对象的值在初始化后无法更改。Constexpr 表示对象必须具有在编译时已知的值。
  • Constexpr 变量是隐式 const。常量变量不是隐式的 constexpr(具有常量表达式初始值设定项的常量整数变量除外)。

Constexpr 函数可以在编译时计算

constexpr 函数是一个函数,其返回值可以在编译时计算。要使函数成为 constexpr 函数,我们只需在返回类型前面使用关键字即可。

#include <iostream>

constexpr int greater(int x, int y) // now a constexpr function
{
    return (x > y ? x : y);
}

int main()
{
    constexpr int x{ 5 };
    constexpr int y{ 6 };

    // We'll explain why we use variable g here later in the lesson
    constexpr int g { greater(x, y) }; // will be evaluated at compile-time

    std::cout << g << " is greater!\n";

    return 0;
}

函数调用将在编译时而不是运行时进行评估。

在编译时计算函数调用时,编译器将计算函数调用的返回值,然后将函数调用替换为返回值。

所以在我们的例子中,调用 将被函数调用的结果替换,即整数值。换句话说,编译器将编译以下内容:

#include <iostream>

int main()
{
    constexpr int x{ 5 };
    constexpr int y{ 6 };

    constexpr int g { 6 }; // greater(x, y) evaluated and replaced with return value 6

    std::cout << g << " is greater!\n";

    return 0;
}

若要符合编译时计算的条件,函数必须具有 constexpr 返回类型,并且在编译时计算时不调用任何非 constexpr 函数。此外,对函数的调用必须具有常量表达式的参数(例如编译时常量变量或文本)。

Constexpr 函数也可以在运行时进行评估

#include <iostream>

constexpr int greater(int x, int y)
{
    return (x > y ? x : y);
}

int main()
{
    int x{ 5 }; // not constexpr
    int y{ 6 }; // not constexpr

    std::cout << greater(x, y) << " is greater!\n"; // will be evaluated at runtime

    return 0;
}

在此示例中,由于参数 和 不是常量表达式,因此无法在编译时解析该函数。但是,该函数仍将在运行时解析,将预期值作为 non-constexpr 返回。

根据 C++ 标准,如果在返回值的地方使用常量表达式,则必须在编译时计算符合编译时计算条件的 constexpr 函数。否则,编译器可以在编译时或运行时自由评估函数。

使用 consteval 使 constexpr 在编译时执行(C++20)

consteval 函数的缺点是此类函数无法在运行时进行计算,这使得它们不如 constexpr 函数灵活,后者可以执行任何操作。

#include <iostream>

// Uses abbreviated function template (C++20) and `auto` return type to make this function work with any type of value
// See 'related content' box below for more info (you don't need to know how these work to use this function)
consteval auto compileTime(auto value)
{
    return value;
}

constexpr int greater(int x, int y) // function is constexpr
{
    return (x > y ? x : y);
}

int main()
{
    std::cout << greater(5, 6) << '\n';              // may or may not execute at compile-time
    std::cout << compileTime(greater(5, 6)) << '\n'; // will execute at compile-time

    int x { 5 };
    std::cout << greater(x, 6) << '\n';              // we can still call the constexpr version at runtime if we wish

    return 0;
}

如果我们使用 constexpr 函数的返回值作为 consteval 函数的参数,则必须在编译时计算 constexpr 函数!consteval 函数只是将此参数作为自己的返回值返回,因此调用方仍然可以使用它。

std::string

先介绍一下 C 样式的字符串文字:

#include <iostream>

int main()
{
    std::cout << "Hello, world!"; // "Hello world!" is a C-style string literal.
    return 0;
}

虽然 C 样式的字符串文本很好用,但 C 样式的字符串变量行为奇怪,难以处理(例如,您不能使用赋值为 C 样式的字符串变量分配一个新值),并且很危险(例如,如果您将较大的 C 样式字符串复制到分配给较短的 C 样式字符串的空间中,则会导致未定义的行为)。

在现代 C++ 中,最好避免使用 C 样式的字符串变量。

幸运的是,C++在语言中引入了两种额外的字符串类型,它们更容易、更安全:
std::string std::string_view

在 C++ 中处理字符串和字符串对象的最简单方法是通过类型,该类型位于 标头中。

我们可以像创建其他对象一样创建类型的对象:

#include <string> // allows use of std::string

int main()
{
    std::string name {}; // empty string

    return 0;
}

就像普通变量一样,您可以按照预期初始化或为 std::string 对象赋值:

#include <string>

int main()
{
    std::string name { "Alex" }; // initialize name with string literal "Alex"
    name = "John";               // change name to "John"

    return 0;
}

字符串输出 std::cout

#include <iostream>
#include <string>

int main()
{
    std::string name { "Alex" };
    std::cout << "My name is: " << name << '\n';

    return 0;
}

在这里插入图片描述

std::string可以处理不同长度的字符串

#include <iostream>
#include <string>

int main()
{
    std::string name { "Alex" }; // initialize name with string literal "Alex"
    std::cout << name << '\n';

    name = "Jason";              // change name to a longer string
    std::cout << name << '\n';

    name = "Jay";                // change name to a shorter string
    std::cout << name << '\n';

    return 0;
}

字符串输入 std::cin

#include <iostream>
#include <string>

int main()
{
    std::cout << "Enter your full name: ";
    std::string name{};
    std::cin >> name; // this won't work as expected since std::cin breaks on whitespace

    std::cout << "Enter your favorite color: ";
    std::string color{};
    std::cin >> color;

    std::cout << "Your name is " << name << " and your favorite color is " << color << '\n';

    return 0;
}

在这里插入图片描述

用于输入文本std::getline()

#include <iostream>
#include <string> // For std::string and std::getline

int main()
{
    std::cout << "Enter your full name: ";
    std::string name{};
    std::getline(std::cin >> std::ws, name); // read a full line of text into name

    std::cout << "Enter your favorite color: ";
    std::string color{};
    std::getline(std::cin >> std::ws, color); // read a full line of text into color

    std::cout << "Your name is " << name << " and your favorite color is " << color << '\n';

    return 0;
}

不要按值传递

每当初始化 std::string 时,都会创建用于初始化它的字符串的副本。制作字符串的副本很昂贵,因此应注意尽量减少制作的副本数量。

当 a 按值传递给函数时,必须实例化函数参数并使用参数进行初始化。这会导致昂贵的副本。

Constexpr 字符串

尝试定义 ,编译器可能会生成错误:constexpr std::string

#include <iostream>
#include <string>

int main()
{
    using namespace std::string_literals;

    constexpr std::string name{ "Alex"s }; // compile error

    std::cout << "My name is: " << name;

    return 0;
}

发生这种情况是因为在 C++17 或更早版本中根本不支持,并且仅在 C++20/23 中非常有限的情况下有效。如果您需要 constexpr 字符串,请改用 std::string_view

std::string_view

为了解决初始化(或复制)字符串成本高昂的问题,引入了 C++17(位于 <string_view> 标头中)。 提供对现有字符串(C 样式字符串、a 或另一个字符串)的只读访问,而无需创建副本。

只读意味着我们可以访问和使用正在查看的值,但我们不能修改它。

下面有两个示例:

使用 std::string

#include <iostream>
#include <string>

void printString(std::string str) // str makes a copy of its initializer
{
    std::cout << str << '\n';
}

int main()
{
    std::string s{ "Hello, world!" }; // s makes a copy of its initializer
    printString(s);

    return 0;
}

使用 std::string_view

#include <iostream>
#include <string_view> // C++17

// str provides read-only access to whatever argument is passed in
void printSV(std::string_view str) // now a std::string_view
{
    std::cout << str << '\n';
}

int main()
{
    std::string_view s{ "Hello, world!" }; // now a std::string_view
    printSV(s);

    return 0;
}

该程序生成与前一个程序相同的输出,但不会创建字符串“Hello, world!”的副本。

当需要只读字符串时,首选 std::string_view,尤其是对于函数参数。

可以使用许多不同类型的字符串进行初始化

关于一个巧妙的事情之一是它的灵活性。

对象可以用 C 样式字符串、a 、 或其他字符串进行初始化:

#include <iostream>
#include <string>
#include <string_view>

int main()
{
    std::string_view s1 { "Hello, world!" }; // initialize with C-style string literal
    std::cout << s1 << '\n';

    std::string s{ "Hello, world!" };
    std::string_view s2 { s };  // initialize with std::string
    std::cout << s2 << '\n';

    std::string_view s3 { s2 }; // initialize with std::string_view
    std::cout << s3 << '\n';

    return 0;
}

可以接受许多不同类型的字符串参数

C 样式字符串和 a 都将隐式转换为 .因此,参数将接受 C 样式字符串:

#include <iostream>
#include <string>
#include <string_view>

void printSV(std::string_view str)
{
    std::cout << str << '\n';
}

int main()
{
    printSV("Hello, world!"); // call with C-style string literal

    std::string s2{ "Hello, world!" };
    printSV(s2); // call with std::string

    std::string_view s3 { s2 };
    printSV(s3); // call with std::string_view

    return 0;
}

std::string_view不会隐式转换为std::string

因为要复制它的初始化项(开销很大),c++不允许a到a的隐式转换。

这是为了防止意外地将实参传递给形参,并在可能不需要这种副本的情况下无意中生成昂贵的副本。

constexpr std::string_view

#include <iostream>
#include <string_view>

int main()
{
    constexpr std::string_view s{ "Hello, world!" }; // s is a string symbolic constant
    std::cout << s << '\n'; // s will be replaced with "Hello, world!" at compile-time

    return 0;
}

std::string_view最好用作只读函数参数

std::string_view std::string 区别

std::string 和 std::string_view 在 C++ 中都用于处理字符串,但它们的用法和性能有所不同:

  1. std::string 是一个动态字符串类,它可以创建、修改和删除字符串。std::string 会分配内存来存储字符串数据,因此在创建和修改字符串时可能会有一定的性能开销。

    std::string str = "Hello, World!";
    str += " How are you?";
    
  2. std::string_view 是 C++17 引入的一个新特性,它提供了一种引用字符串(或字符串的一部分)的轻量级方式,而无需复制字符串。std::string_view 不会分配内存,也不会拷贝字符串,因此在处理大字符串或者需要频繁修改字符串的场景下,std::string_view 可以提供更好的性能。

    std::string str = "Hello, World!";
    std::string_view sv = str;
    

需要注意的是,std::string_view 只是引用了字符串,而不拥有它。如果原始字符串被修改或删除,std::string_view 可能会引用到无效的内存。因此,std::string_view 最好只在你确定原始字符串不会被修改或删除的情况下使用。

std::string_view 和 std::string 哪个更适合用于函数参数传递

  1. 如果你的函数只需要读取字符串,不需要修改它,那么 std::string_view 是一个更好的选择。std::string_view 可以接受 std::string 和 C 风格字符串,而且不会产生额外的内存分配或字符串复制,因此性能更好。

    void print_string(std::string_view sv) {
     std::cout << sv << std::endl;
    }
    
  2. 如果你的函数需要修改字符串,或者需要保留字符串作为返回值或存储在数据结构中,那么你应该使用 std::string。std::string 拥有它的数据,因此你可以安全地修改它,而不用担心原始字符串被修改或删除。

    std::string to_upper(std::string str) {
      for (auto& c : str) {
    	    c = std::toupper(c);
    	}
     return str;
    }
    
  3. 如果你的函数需要以 null 结尾的字符串,例如需要传递给需要以 null 结尾的字符串的 C 函数,那么你应该使用 std::string。std::string 保证字符串以 null 结尾,而 std::string_view 不提供这个保证。

  • 28
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值