常量和字符串
- 常量变量
- 常量表达式
- 编译时优化
- Constexpr 变量
- std::string
- 字符串输出 std::cout
- std::string可以处理不同长度的字符串
- 字符串输入 std::cin
- 用于输入文本std::getline()
- 不要按值传递
- Constexpr 字符串
- std::string_view
- 可以使用许多不同类型的字符串进行初始化
- 可以接受许多不同类型的字符串参数
- std::string_view不会隐式转换为std::string
- constexpr std::string_view
- std::string_view std::string 区别
- std::string_view 和 std::string 哪个更适合用于函数参数传递
常量变量
在编程中,常量是在程序执行期间不得更改的值。
在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++ 中都用于处理字符串,但它们的用法和性能有所不同:
-
std::string 是一个动态字符串类,它可以创建、修改和删除字符串。std::string 会分配内存来存储字符串数据,因此在创建和修改字符串时可能会有一定的性能开销。
std::string str = "Hello, World!"; str += " How are you?";
-
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 哪个更适合用于函数参数传递
-
如果你的函数只需要
读取字符串
,不需要修改它,那么std::string_view
是一个更好的选择。std::string_view 可以接受 std::string 和 C 风格字符串,而且不会产生额外的内存分配或字符串复制,因此性能更好。void print_string(std::string_view sv) { std::cout << sv << std::endl; }
-
如果你的函数需要
修改字符串
,或者需要保留字符串作为返回值或存储在数据结构中,那么你应该使用std::string
。std::string 拥有它的数据,因此你可以安全地修改它,而不用担心原始字符串被修改或删除。std::string to_upper(std::string str) { for (auto& c : str) { c = std::toupper(c); } return str; }
-
如果你的函数需要以
null 结尾的字符串
,例如需要传递给需要以 null 结尾的字符串的 C 函数,那么你应该使用std::string
。std::string 保证字符串以 null 结尾,而 std::string_view 不提供这个保证。