一、前言
在正常的开发过程中,使用标准库是一件非常常见的事情,如果不会使用标准库,程序开发效率将大幅下降。
为了后面做一些有趣的小项目,所以本节将讲一些C/C++
中常用的标准库。
但这也只是为了给大家留下一个印象,更多的还是需要大家自己学习。
最主要的原因是,如果不经常用,即使现在会用了,过段时间也会忘了,所以如果后面的项目中遇到我觉得大家可能不怎么熟悉的地方,也会稍微讲一讲。
二、String
C/C++的标准库众多,所以我们就先挑一个稍微简单一点且非常常用的标准库:string
。
不过由于C/C++
标准库里面的源码到处使用宏、自定义结构体和模板,十分晦涩难懂,我们这里只是简单实现一下它的基础功能即可。
老规矩,不知不觉已经来到第七章了,那就新建一个项目day7
,将其设为启动项目、并添加源文件main.cpp
:
看名字string
就知道,这是一个字符串,其本质就是封装了对char*
的操作。
那么让我们先来使用一下它,看它有多方便吧,添加头文件string
:
#include<iostream>
#include<string>
using std::string;
using std::cout;
using std::endl;
int main() {
string s1="https://www.kucoding.com";
s1 += " 学习C/C++";
cout << s1 << endl;
}
同样的,string
也是在标准命名空间std
中,避免写着麻烦,我们就使用了using std::string
。
这里用到了一个写法 +=
,这其实也是一个运算符,等效于 s1=s1+" 学习C/C++";
,其实int
,double
等基本数据类型都可以这样写。
上面的几句话,如果换为char*
,使用C语言提供的字符串操作函数,就是下面这种写法:
#include<iostream>
using std::cout;
using std::endl;
int main() {
char buf[100]="https://www.kucoding.com";
strcat_s(buf,100," 学习C/C++");
cout << buf<<endl;
}
strcat_s
为函数strcat
的安全函数,用于拼接字符串。
这么一看,好像string
标准库也没什么优势,但如果继续下去,你就会发现string
的优越性。
比如:
- 我这里给缓存区
buf
给了100
个空间,这是我估计出来的,但string
不用我来估计。 - 只要电脑内存够大,
string
几乎可以无限自动追加,且这一过程是自动的,虽然我们用char*
,通过new
动态分配内存的方法也可以实现,但全程需要我们手动操作。 string
只需要像符合我们常规思路的使用即可比如用+=
追加字符串,但使用char*
,我们得自己记大量的字符串操作函数。
所以接下来,我们就试着实现一个自己的string
类,来实现一个常规的字符串类的基本功能。
首先,创建一个类String
,它需要有三个成员变量:
-
buf
:存放数据的缓存区 -
size
:当前字符串的长度(不含末尾0结束字符)
其目的是方便成员函数使用,比如增、删、改、查等操作都需要字符串的长度才能进行,与其每次都调用一下strlen
函数来计算,不如直接记录下来。
capacity
:缓存区容量,即缓存区的大小,方便时刻关注当前操作是否有可能超出缓存区的大小。
完成之后如下:
class String {
private:
char* buf; //存放数据的缓存区
int size; //当前使用了多少缓存区
int capacity; //缓存区的容量
public:
};
有了成员变量,我们就可以开始写它的构造函数了:
String() {
buf = new char[15];
capacity = 15;
size = 0;
}
String(const char* str) {
size=strlen(str);
capacity = size * 2;
buf = new char[capacity];
strcpy_s(buf, size+1, str);
}
这里只写了两个构造函数,分别为无参构造函数与带一个常量字符串参数的构造函数。
- 无参构造函数:给缓存区申请默认大小
15
个字节,并给成员变量赋值。 - 带参构造函数:用于将传入的字符串赋值给本类中的缓存区,并初始化
size
与capacity
,capacity
之所以要比size
大两倍,是为了方便后面追加字符串。
这里用到了C语言的函数strlen
,作用是用于求字符串的长度,注意它得到的长度不包含末尾\0
。
还有strcpy_s
为strcpy
的安全函数,用于复制字符串,将第三个参数的字符串,复制到第一个所指定的缓存区中,中间的大小要包含最后结尾字符 \0
,所以要加一。
写完构造函数,再把析构函数写好:
~String()
{
if (buf != NULL) {
delete[] buf;
}
}
开始先进行了判断,如果buf
等于NULL
,就说明它没有被分配内存,就不进行delete[]
操作。
这里使用delete[]
而不是delete
的原因是删除的是数组,一般来说,两者是匹配使用的,不理解的可以参考文章:内存分配。
然后是写赋值函数:
void operator=(const char* str) {
if (buf != NULL) {
delete[] buf;
buf = NULL;
}
size = strlen(str);
capacity = size * 2;
buf = new char[capacity];
strcpy_s(buf, size+1, str);
}
这又是一个新知识点,叫做重载运算符,通过关键字operator
进行,具体如何写,取决于紧跟其后的操作符号。
比如这里是等于号=
,等于号有左右两个操作数,但由于这是在类里面,类本身占了一个左操作数,所以还需要一个参数作为右操作数。
各个运算符用法不一定相同,请结合具体情况。
之所以参数为const char *
,仅仅只是因为我们需要的是赋值字符串,如果你有其它需求,完全可以改。
毕竟是赋值动作,所以得先判断以前是不是有缓存区,如果有就先删除了,然后重新分配空间,拷贝过来。
至于返回值为什么为void
,仅仅只是不想弄的太复杂,当然你也可以添加。
之所以C++之中能使用这种连等的语法:
a=b=c=d
,就是依靠的重载等于运算符,然后返回一个返回值,不过我这里就不深入了,可以自己尝试一下。
接下来再看一看我们上面使用的 +=
,这同样是一个操作符号:
void operator+=(const char* str) {
int tLen = strlen(str);
if (buf == NULL) {
size = tLen;
capacity = size * 2;
buf = new char[capacity];
strcpy_s(buf, size+1, str);
}
else if(tLen+size+1<capacity) {
size += tLen;
strcat_s(buf, size+1, str);
}
else {
capacity = (size + tLen) * 2;
char* tBuf = new char[capacity];
tBuf[0] = '\0';
strcat_s(tBuf, size+1, buf);
strcat_s(tBuf, size + tLen+1, str);
size += tLen;
delete[] buf;
buf = tBuf;
}
}
这个就稍微复杂一些,它的用法和前面的=
符号是一样的。
函数中先是得到字符串长度tLen
,然后判断缓存区是否存在,如果不存在,就和前面的那个带参构造函数中代码相同。
如果已经存在,就再看已用的空间加上追加的字符串长度,是否小于容量。
如果小于,我们就可以直接用函数strcat_s
来进行追加字符串。
strcat_s
函数为strcat
函数安全函数,只是多了第二个参数,指定追加完之后字符串的长度,注意要加1
,因为有结尾 \0
。
否则,就是容量不够了,那就重新开辟一个更大的空间,然后将两个字符串都追加到新的空间中,删除原有空间,将新空间赋值给buf
使用。
这里注意我将新空间的第一个元素赋值为 \0
,这是因为strcat_s
函数是通过结尾字符\0
开始追加的。
char* tBuf = new char[capacity];
tBuf[0] = '\0';
最后,还需要一个获取大小,容量的函数:
int Size() {
return size;
}
int Capacity() {
return capacity;
}