原创
std::string
2010-4-2 22:11
5953
9
9
分类:
工程师职场
本文主要针对那些有C语言背景知识,而现在开始使用C++语言编程的程序员。事实上,C++继承了大多数C语言的功能,但有些方面还是不得不要留意的,如new和delete取代了malloc和free,且C++还使用了STL容器类来静态或动态地分配数组。本文中要讲的是用std::string来取代char*,将会演示C风格数组带来的一系列问题,及如何使用std::string来避免这些问题。 避免“病态”的char数组声明 当声明一个char数组时,许多程序员都会这样做: char* name = "marius"; 乍看起来好像没什么问题,但如果想让字符串首字符大写,最简单的实现方法是: name[0] = 'M'; 代码生成时没有问题,但在运行时会崩溃,因为这是未定义的行为,且依赖于编译器的实现(在VS2005中,可通过编译,但在运行时会崩溃)。对此的解答是:“marius”是一个文字上的字符串,且存储于程序的数据区,“name”只是一个指向数组的指针,因为存储字符串的数据区为只读,所以不允许你修改它。正确的声明形式应该像下面这样: const char* name = "marius"; 这样一来,只要试图修改其中的一个字符,都会被编译器发现,并抛出一个错误:cannot modify a constant variable。 “令人讨厌”的C风格方法 可用char[]来定义一个定长的字符数组: char name[] = "marius"; name[0] = 'M'; 在本例中,name是一个7字符的数组(包括终止符),其由字符串“marius”进行初始化,具有读写权限。 现在,试着用strcat()衔接一个字符串: char name[] = "marius"; strcat(name, " bancila"); 但程序只要一运行就会崩溃,因为strcat不能确定缓冲区是否可以装下追加的字符串,导致数组越界破坏了内存。 当然了,你也可声明一个更大的数组来解决这个问题,只要保证它能放下所有的字符就行了,比如说,50个字符长度应该可以放下一个英文名了: char name[50] = "marius"; strcat(name, " bancila"); 这就行了,但如果有Carlos Marìa Eduardo García de la Cal Fernàndez Leal Luna Delgado Galván Sanz这样的名字呢,而且这只是单个西班牙名,另外还有内存空间浪费的问题,如果声明了100个字符长度,平均使用只有20,那一份十万个名字的列表,要浪费800万字节了。 动态分配内存 那么接下来就是寻找动态分配内存最合适的方法: char* name = new char[strlen("marius")+1]; strcpy(name, "marius"); 在此例中,你可重新分配所需的内存,如下所示: char* temp = new char[strlen(name) + strlen(" bancila") + 1]; strcpy(temp, name); strcat(temp, " bancila"); delete [] name; name = temp; 这需要编写及维护更多的代码,另外,在涉及到类时,情况会变得更加复杂。 确保类中内存的正确处理 如果有一个Person类,它存储了人名,你的第一个反应它可能会像下面这样: class Person { char* name; }; 好像看上去没什么问题,但这个类还应有: ? 一个构造函数,它可以接受一个字符串来初始化name; ? 一个自定义的拷贝构造函数,以确保深拷贝(默认的拷贝构造函数由编译器提供,它是浅拷贝,也就是说,当从一个对象中复制全部属性的值到一个对象时,它只复制了指针,而不是指向的所有对象)。 ? 一个自定义的 operator= ? 一个析构函数,负责清理动态分配的内存 把这些整合起来之后,Person类就会像下面这样: class Person { char* name; public: Person(const char* str) { name = new char [strlen(str)+1]; strcpy(name, str); } Person(const Person& p) { name = new char [strlen(p.name)+1]; } Person& operator=(const Person& p) { if(this != &p) { delete [] name; name = new char [strlen(p.name)+1]; strcpy(name, p.name); } return *this; } ~Person() { delete [] name; } }; 还是std::string省事 标准模板库(STL)提供了一个std::string类,其是std::basic_string的一个特化,它是一个容器类,可把字符串当作普通类型来使用,并支持比较、连接、遍历、STL算法、复制、赋值等等操作,这个类定义在<string>头文件中。 使用std::string的好处在于: 1、 易于分配、复制及连接。 std::string name = "marius"; // 由赋值进行初始化 name += " bancila"; // 连接 std::string copy = name; // 复制 2、 可用length()或size()方法确定字符串的长度,这两个方法是一样的,第二个方法只是为了保持STL容器类的一致性。 std::string name = "marius"; std::cout << "length=" << name.length() << std::endl; std::cout << "length=" << name.size() << std::endl; 3、 检查是否为空值。 std::string name; if(name.empty()) std::cout << "empty string"; 4、 支持比较。 if(name == "marius") { } if(name.compare("marius") == 0) { } 方法campre进行大小写敏感的比较,以确定两个字符串是否相等,或其中一个在词典顺序上小于另一个。它的返回值与strcmp()的返回值代表的意义一样:负值表示操作数小于参数字符串,而正值表示操作系统数大于它,0表示相等。另外,还有6个重载版本可允许比较字符串的某一部分: if(name.compare(0, 3, "mar") == 0) { std::cout << "match"; } 5、 重载操作符 << 和 >>,可从流中读写字符串。 std::string name; std::cin >> name; // 从控制台中读name std::cout << name; // 向控制台写name 6、 易于访问字符串中的字符。 std::string name = "marius"; name[0] = 'M'; name[name.length()-1] = 'S'; 7、 遍历所有字符,这可由C风格的索引或STL迭代子来完成(如果无需修改,应使用const_iterator)。 std::string name = "marius"; for(size_t i = 0; i < name.length(); ++i) std::cout << name; for(std::string::const_iterator cit = name.begin(); cit != name.end(); ++cit) std::cout << *cit; for(std::string::iterator it = name.begin();it != name.end(); ++it) *it = toupper(*it); 8、 删除字符串的某一部分。 std::string name = "marius bancila"; // 删除第6个元素之后的所有东西 name.erase(6, name.length() - 6); 9、 在指定位置插入字符串或字符。 std::string name = "marius"; // 在结尾插入 name.insert(name.length(), " bancila"); name.insert(name.length(), 3, '!'); 10、在字符串结尾插入其他元素。 std::string name = "marius"; name.push_back('!'); 11、两个字符串值的快速交换。 std::string firstname = "bancila"; std::string lastname = "marius"; firstname.swap(lastname); std::cout << firstname << ' ' << lastname << std::endl; 12、使用c_str()方法只读访问其内部字符数组缓冲区,可在接受字符指针(是否const都行)作参数的函数中使用std::string对象。 void print(const char* name) { std::cout << name << std::endl; } std::string name = "marius"; print(name.c_str()); void makeupper(char* array, int len) { for(int i = 0; i < len; ++i) array = toupper(array); } std::string name = "marius"; makeupper(&name[0], name.length()); 13、使用STL算法 std::string name = "marius"; // 使字符串全为大写 std::transform(name.begin(), name.end(), name.begin(),toupper); std::string name = "marius"; // 升序排列字符串 std::sort(name.begin(), name.end()); std::string name = "marius"; // 反转字符串 std::reverse(name.begin(), name.end()); bool iswhitespace(char ch) { return ch == ' ' || ch == '\t' || ch == '\v' || ch == '\r' || ch == '\n'; } std::string name = " marius "; // 删除空白字符 std::string::iterator newend = std::remove_if(name.begin(), name.end(), iswhitespace); name.erase(newend); 14、也可用头文件<sstream>中的std::stringstream来构建字符串。 std::stringstream strbuilder; strbuilder << "1 + 1 = " << 1+1; std::string str = strbuilder.str(); 来回顾一下前面的Person类,如果用std::string替换了char*,那么剩下的工作只需编写一个构造函数就行了,其他的由编译器来完成,在本例中,复制字符串时使用了浅拷贝,这足够了,因为这个动作触发了std::string的operator=,它会正确地复制字符串。 class Person { std::string name; public: Person(const std::string& str) { name = str; } }; Person p1("marius"); // works because std::string has a constructor that takes a const // char* Person p2("bancila"); p1 = p2; 结论 本文既不是std::string的文档,也不是其辅导书,只是恳求大家使用std::string。用标准模板库中的std::string来取代C风格数组可使代码看上去更简洁、更自然、更易于阅读及维护,也不必担心动态内存分配等问题,由此可忽略一些不必要的细节问题(如内存管理),而集中精力于编程的重要方面,试下吧。
文章评论(0条评论)
登录后参与讨论