十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

使用string和string_view(一)——C风格字符串、字符串字面量和std::string

使用string和string_view(一)——C风格字符串、字符串字面量和std::string 目录1. C风格字符串2. 字符串字面量2.1 简介2.2 原始字符串字面量3. std::string3.1 C风格字符串的问题3.2 使用string类3.3 字符串比较3.4 内存处理3.5 与C风格字符串的兼容3.6 std::string字面量3.7 std::vector和字符串的CTAD参考1. C风格字符串在C语言中字符串表示为字符的数组。字符串中的最后一个字符是null字符(\0)这样操作字符串的代码就知道字符串在哪里结束。官方将这个null字符定义为NUL这个拼写中只有一个L而不是两个L。NUL和NULL指针是两回事。尽管C提供了更好的字符串抽象但理解C语言中使用的字符串技术非常重要因为在C程序设计中仍可能使用这些技术。最常见的一种情况是C程序调用某个第三方库或与操作系统交互时调用基于C语言的接口。目前程序员使用C字符串时最常犯的错误是忘记为\0字符分配空间。例如字符串hello看上去有5个字符长但在内存中需要6个字符的空间才能保存这个字符串的值。C包含一些来自C语言的字符串操作函数它们在cstring头文件中定义。通常这些函数不直接操作内存分配。例如strcpy()函数有两个字符串参数。这个函数将第二个字符串复制到第一个字符串而不考虑第二个字符串能否恰当地填入第一个字符串。下面的代码试图在strcpy()函数之上构建一个包装器这个包装器能够分配正确数量的内存并返回结果而不是接收一个已经分配好的字符串。这个最初的尝试会被证明是错误的函数通过strlen()函数获取字符串的长度。调用者负责释放由copyString()分配的内存。char*copyString(constchar*str){char*result{newchar[strlen(str)]};// bug! off by one!strcpy(result,str);returnresult;}copyString()函数的代码这样写是不正确的。strlen()函数返回字符串的长度而不是保存这个字符串所需的内存量。对于字符串hellostrlen()返回的是5而不是6。为字符串分配内存的正确方式是在实际字符所需的空间加1。一开始看到到处都需要加1可能会感到有点奇怪但这是其工作方式所以在使用C风格字符串时要记住这一点。正确实现的代码如下char*copyString(constchar*str){char*result{newchar[strlen(str)1]};strcpy(result,str);returnresult;}要记住strlen()只返回字符串中实际字符数的一种方式是考虑如果为一个由几个其他字符串构成的字符串分配空间应该怎么做。例如如果函数接收3个字符串参数并返回一个由这3个字符串串联而成的字符串那么这个返回的字符串应该有多大为精确分配足够空间空间的大小应该是3个字符串的长度相加然后加上1留给尾部的\0字符。如果strlen()的字符串长度包括\0那么分配的内存就会过大。下面的代码通过strcpy()和strcat()函数执行这个操作char*appendStrings(constchar*str1,constchar*str2,constchar*str3){char*result{newchar[strlen(str1)strlen(str2)strlen(str3)1]};strcpy(result,str1);strcat(result,str2);strcat(result,str3);returnresult;}C和C中的sizeof()操作符可用于获得给定数据类型或变量的大小。例如sizeof(char)返回1因为字符的大小是1字节。但在C风格字符串中sizeof()和strlen()是不同的。绝对不要通过sizeof()获取字符串的大小。它根据C风格的字符串的存储方式来返回大小。如果C风格字符串存储为char[]则sizeof()返回字符串使用的实际内存包括\0字符。例如chartext1[]{abcdef};size_t s1{sizeof(text1)};// is 7.size_t s2{strlen(text1)};// is 6.但是如果C风格字符串存储为char*sizeof()就返回指针的大小例如constchar*text2{abcdef};size_t s3{sizeof(text2)};// is platform-dependent.size_t s4{strlen(text2)};// is 6.在32位模式下编译期s3的值为4而在64位模式下编译期s3的值为8因为这返回的是指针const char*的大小。警告在Microsoft Visual Studio中使用C风格字符串函数时编译器可能会给出安全相关的警告甚至错误说明这些函数已经被弃用了。使用其他C标准库函数可以避免这些警告例如strcpy_s()和strcat_s()这些函数是安全C库(ISO/IEC TR 24731)标准的一部分。然而最好的解决方案是切换到C的string类。2. 字符串字面量2.1 简介注意C程序编写的字符串要用引号包围。例如下面的代码输出字符串hello这段代码包含这个字符串本身而不是一个包含这个字符串的变量。std::cout hello std::endl;在上面的代码中hello是一个字符串字面值因为这个字符串以值的形式写出而不是一个变量。字符串字面量实际上存储在内存的只读部分。通过这种方式编译器可重用相同字符串字面量的引用从而优化内存的使用。也就是说即使一个程序使用了500次hello字符串字面量编译器也只在内存中创建一个hello实例。这种技术称为字面量池。字符串字面值可赋值给变量但因为字符串字面量位于内存的只读部分且使用了字面量池所以这样做会产生风险。C标准正式指出字符串字面量的类型为n个const char数组然而为了向后兼容较老的不支持const的代码大部分编译器不会强制程序将字符串字面量赋值给const char*类型的变量。这些编译器允许将字符串字面量赋值给不带有const的char*而且整个程序可正常运行除非试图修改字符串。一般情况下试图修改字符串字面量的行为是没有定义的。可能会导致程序崩溃可能使程序继续执行看起来却有莫名其妙的副作用可能不加通告地忽略修改行为可能修改行为是有效的这完全取决于编译器。例如下面的代码展示了未定义的行为char*ptr{hello};// assign the string literal to a variable.ptr[1]a;// undefined behavior!一种更安全的编码方式是在引用字符串常量时使用指向const字符的指针。下面的代码包含同样的bug但由于这段代码将字符串字面量赋值给const char*因此编译器会捕获到任何写入只读内存的企图。constchar*ptr{hello};// assign the string literal to a variable.ptr[1]a;// error! attempts to write to read-only memory.还可将字符串字面量用作字符数组char[]的初始值。这种情况下编译器会创建一个足以放下这个字符串的数组然后将字符串复制到这个数组。因此编译器不会将字面量放在只读的内存中也不会进行字面量池的操作。chararr[]{hello};// compiler takes care of creating appropriate sized// character array arr.arr[1]a;// the contents can be modified.2.2 原始字符串字面量原始字符串字面量是可横跨多行代码的字符串字面量不需要转义嵌入的双引号像\t和\n这种转义序列不按照转义序列的方式处理而是按照普通文本的方式处理。如果像下面这样编写普通的字符串字面量那么会收到一个编译器错误因为字符串中包含了未转义的双引号。const char* str { Hello World! }; / error!对于普通字符串必须转义双引号如下所示。const char* str { Hello \World\! };对于原始字符串字面量就不需要转义双引号了。原始字符串字面量以R“(开头以)结尾。const char* str { R(Hello World!) };如果需要一个包含多行的字符串不使用原始字符串字面量的话就需要在字符串中新行的开始位置嵌入\n转义序列。例如const char* str { Line 1\nLine 2 };如果将这个字符串输出到控制台将看到以下结果。Line1Line2而使用原始字符串字面量不使用\n转义序列来开始一个新行只需要在源代码中按下Enter键以开始一个真正的新行。这与前面使用嵌入的\n的代码片段的效果相同。constchar*str{R(Line 1 Line 2)};在原始字符串字面量中忽略了转义序列。例如在下面的原始字符串字面量中\t转义序列没有替换为实际的制表符字符而是按照字面形式保存。const char* str { R(Is the following a tab character? \t) };因此如果将此字符串输出到控制台将看到以下结果。Is the following a tab character?\t因为原始字符串字面量以)“结尾所以使用这种语法时不能在字符串中嵌入)”。例如下面的字符串是不合法的因为在这个字符串的中间包含)“。const char* str { R(Embedded ) characters) } // error!如果需要嵌入)”则需要使用扩展的原始字符串字面量语法如下所示。Rd-char-sequence(r-char-sequence)d-char-sequencer-char-sequence是实际的原始字符串。d-char-sequence是可选的分隔符序列原始字符串首尾的分隔符序列应该一致。分隔符序列最多能有16个字符。应选择未出现在原始字符串字面量中的序列作为分隔符序列。上例可改用唯一的分隔符序列。const char* str { R-((Embedded ) characters)- };在操作数据库查询字符串、正则表达式和文件路径时原始字符串字面量可以令程序的编写更加方便。3. std::stringC提供了一个得到极大改善的字符串概念并作为标准库的一部分提供了这个字符串的实现。在C中std::string是一个类(实际上是basic_string模板类的一个实例)这个类支持cstring中提供的许多功能还能自动管理内存分配。string类在std名称空间的string头文件中定义。3.1 C风格字符串的问题为理解C String类的必要性需要考虑C风格字符串的优势和劣势。优势1. 很简单底层使用了基本的字符类型和数组结构。2. 轻量级如果使用得当只会占用所需的内存。3. 可按操作原始内存的方式轻松操作和复制字符串。4. 如果你是一名C语言程序员为什么还要学习新事物劣势1. 为了模拟一等数据类型字符串需要付出很多努力。2. 很容易产生难以找到的内存bug且难以解决。3. 没有利用C的面向对象特性。4. 要求程序员了解底层的表示方式。C的String类解决了C风格字符串的所有问题并且证明C风格字符串相比一等数据类型的那些优势实际上是无关紧要的。3.2 使用string类尽管string是一个类但是几乎总可将string当成内建类型使用。事实上把string想象成简单类型更容易发挥string的作用。通过运算符重载的神奇作用C的string使用起来比C字符串容易得多。例如给string重新定义运算符以表示字符串串联。下面的例子会得到1234std::string a{12};std::string b{34};std::string c;cab;// c is 1234.运算符也被重载了通过这个运算符可以轻松地追加一个字符串。a b; // a is 1234.3.3 字符串比较C风格字符串的另一个问题是不能通过运算符进行比较。假设有以下两个字符串。char*a{12};charb[]{12};按照下述方式编写的比较操作始终返回false因为它比较的是指针的值而不是字符串的内容。if(ab){/* ... */}注意C数组和指针是相关的。可将C数组看成指向数组中第一个元素的指针。要比较C字符串需要编写这样的代码if(strcmp(a,b)0){/* ... */}此外C字符串也无法通过、、或进行比较因此需要通过strcmp()根据字符串的字典顺序返回-1、0和1的值判断。这样会产生非常笨拙且可读性低的代码还很容易出错。在C的String类中操作符都被重载了这些运算符可以操作真正的字符串字符。单独的字符可通过方括号运算符[]访问。C string类另外提供了一个compare()方法它的行为类似于strcmp()并且具有类似的返回类型。实例如下std::string a{12};std::string b{34};autoresult{a.compare(b)};if(result0){std::coutless\n;}elseif(result0){std::coutequal\n;}else{std::coutgreater\n;}与strcmp()一样这样使用起来很麻烦你需要记住返回值的确切含义。此外由于返回值只是一个整数很容易忘记这个整数的含义写出以下的错误代码来比较相等性。if(a.compare(b)){std::coutequal\n;}compare()为相等返回0为不相等返回任何其他值。所以这行代码与它的意图相反也就是说它对不相等的字符串输出equal如果只想检查两个字符串是否相等不要使用compare()只需要使用。C20通过三向比较运算符改进了这一切。字符串类完全支持这个运算符示例如下autoresult{ab};if(std::is_lt(result)){std::coutless\n;}if(std::is_gt(result)){std::coutgreater\n;}if(std::is_eq(result)){std::coutequal\n;}3.4 内存处理如下面的代码所示当string操作需要扩展string时string类能够自动处理内存需求因此不会出现内存溢出的情况了。std::string myString{hello};myString, there;std::string myOtherString{myString};if(myStringmyOtherString){myOtherString[0]H;}std::coutmyString\n;std::coutmyOtherString\n;这段代码的输出如下所示hello, there Hello, there在这个例子中有几点需要注意。一是注意即使字符串被分配和调整大小也不会出现内存泄漏的情况。所有这些string对象都创建为栈中的变量。尽管string类肯定需要完成大量分配内存和调整大小的工作但是string类的析构函数会在string对象离开作用域时清理内存。另外需要注意的是运算符以预期的方式工作。例如运算符复制字符串这是最有可能预期的操作。如果习惯使用基于数组的字符串那么这种方式有可能带来全新的体验也可能令你迷惑。不用担心一旦学会信任string类能做出正确的行为那么代码的编写会简单得多。3.5 与C风格字符串的兼容string类支持一系列其他的操作以下是一部分1. substr(pos, len)返回从给定位置开始的给定长度的子字符串。2. find(str)如果找到了字符串返回它的位置如果没有找到返回string::npos。3. replace(pos, len, str)将字符串的一部分(给定开始位置和长度)替换为另一个字符串。4. starts_with(str)/ends_with(str)如果一个字符串以给定的子串开始或结尾则返回true。下面是一个小代码片段展示了其中的一些操作。std::string strHello{Hello!!};std::string strWorld{The World...};autoposition{strHello.find(!!)};if(position!std::string::npos){// found the !! substring, now replace it.strHello.replace(position,2,strWorld.substr(3,6));}std::coutstrHello\n;输出如下Hello World注意从C20开始std::string是constexpr类这意味着string可用于在编译期执行操作并可用于constexpr函数和类的实现。3.6 std::string字面量源代码中的字符串字面量通常解释为const char*。使用用户定义的标准字面量s可以把字符串字面量解释为std::string。例如autostring1{Hello World};// string1 is a const char*.autostring2{Hello Worlds};// string2 is an std::string.标准用户定义字面量s在std::literals::string_literals名称空间中定义。但是string_literals和literals名称空间都是所谓的内联名称空间。因此使用以下选项可以使这些字符串字面量可用于你的代码。usingnamespacestd;usingnamespacestd::literals;usingnamespacestd::string_literals;usingnamespacestd::literals::string_literals;基本上在内联名称空间中声明的所有内容都会自动在父名称空间可用。要自己定义内联名称空间可以使用inline关键字。例如string_literals内联名称空间定义如下namespacestd{inlinenamespaceliterals{inlinenamespacestring_literals{// ...}}}3.7 std::vector和字符串的CTADstd::vector支持类模板参数推导CTAD允许编译器根据初始化列表自动推导vector的类型对字符串vector使用CTAD时必须小心。以vector的以下声明为例std::vector names{John,Sam,Joe};推导出的类型将是vectorconst char*而不是vectorstd::string这是一个很容易犯的错误可能导致代码出现一些奇怪的行为甚至崩溃。这取决于之后对vector的处理方式。如果你需要一个vectorstd::string可以使用上面一节提到的std::string字面量。注意下例中每个字符串字面量后面的sstd::vector names { Johns, Sams, Joes };参考[比] 马克·格雷戈勒著 程序喵大人 惠惠 墨梵 译 C20高级编程(第五版)
返回列表