菜鸟的C++ 知识盲区(跌倒)到知识发现(爬起)---------第三章 字符串 向量 数组

最近忙了好久,实在没时间写,但是我还要坚持下去,虽然阅读量很低,我也不在意。无所谓了~C++ 大不了黑洞呗!!!~~~

3.1 命名空间的using声明

头文件主要哈,不要包含using声明,是因为头文件的内容会拷贝到所有引用它的文件中去,如果头文件里面有某个using申明,那么使用了该头文件的文件就会都有这个申明。对于某些程序来说,是最不希望看到的,由于不经意包含了一些名字,然而会产生史料未及的名字冲突。

using namespace::name ;

有了上面的声明,就可以直接访问命名空间的名字。

3.2标准库类型 string

标准库类型string表示可变长的字符序列,其定义在std的命名空间里。

初始化string对象的方式:

其实初始化主要有两种方式。如果使用一个等号=初始化一个变量,实际上执行的是一个拷贝初始化,编译器把等号右边的初始值拷贝到新创建的对象中去。与之相反,则执行的是直接初始化

string s5 = "hiya"; // copy initialization
string s6("hiya"); // direct initialization
string s7(10, 'c'); // direct initialization; s7 is cccccccccc

string对象上的操作:

 

// Note: #include and using declarations must be added to compile this code
int main()
{
string s; // empty string
cin >> s; // read a whitespace-separated string into s
cout << s << endl; // write s to the output
return 0;
}

string对象会自动忽略开头的空白(空格、换标符号、制表符号)并从第一个真正的字符开始读起,直到遇见下一个空白字符为止。如果你的程序输入的是:“          hello world ”,前面的空格是输不出来的。有时候我们需要最终得到的字符串保留输入空白符号,这时候用getline函数代替>>的操作。

size函数返回string对象的长度(即是string对象字符的个数),返回的是无符号整型。

string对象的比较规则:

1如果s1长度小于s2长度,且s1的每个字符和s2对应,那么s1<s2。

2如果两个string对象在某些对应的位置不一致,则string'对象比较的结果其实是string对象中的第一对相互不同的字符的比较结果。

字面值和string对象相加:

当 把string对象和字符字面值及字符串字面值混在一条语句的时候使用时~必须确保每个加法运算符的两侧的运算对象至少包含一个是string!!!!!!!!!!!!!!!

string s4 = s1 + ", "; // ok: adding a string and a literal
string s5 = "hello" + ", "; // error: no string operand
string s6 = s1 + ", " + "world"; // ok: each + has a string operand
string s7 = "hello" + ", " + s2; // error: can't add string literals

注意看有人问上述代码的第三行和第四行的区别,其实低三行是连续相加,第二个加号左边是一个string对象,而第四行第一个加号两边都不是string对象~。真的很是个小的容易犯的地方,所以最好对于string对象 连续相加的添加括号。字符串字面值是一串常量字符,字符串字面值常量用双引号括起来的零个或多个字符表示,为兼容C语言,C++中所有的字符串字面值都由编译器自动在末尾添加一个空字符。而字符串常量并不是string的对象!!!

这里我就想多说几几句了! 

参照:

1、string作为一个类在c++中存在。

2、但是char是作为一个基本类型,eg:int 。但是C++中string不是其基本类型~。

3、C风格字符串是以'\0'字符结尾的字符数组。char *s1="abc" char *s2="abc",s1和s2其实指向的是常量区中同一个字符串字面值的首地址。而char s[] = "abc"确切的说是用字符串字面值初始化了一个字符数组,char s1[] = "abc"  char s2[] = "abc"是用同一个字符串字面值初始化了两个不同的字符数组,这里初始化的两个字符数组是可以修改的。你如果比较s1==s2,数组名字隐式转换为首地址,那么会得到false,因为指向的都不是同一个地方,这个时候你在用指针指向首地址进行修改操作就是合法的了

4、char * 是传统的基本串类型,带尾"\0"的字符串,要一套专门的处理串的子程序;string 是面象对象的串对象,封装了许多函数,功能更强;这两个类型可以互相转换,但这是不同的类型,有的传统老式函数ifstream.open它的参数类型是char *,你就不能用string.

5、字符串字面值是一串常量字符,字符串字面值常量用双引号括起来的零个或多个字符表示,为兼容C语言,C++中所有的字符串字面值都由编译器自动在末尾添加一个空字符。

处理string对象的字符:

C++中还增加了:范围for(name for)语句。这种语句会遍历序列中的每个元素并对序列中的每个执行某种操作,其语法形式是:

for (declaration : expression)
statement


string str("some string"); // print the characters in str one character to a line
for (auto c : str) // for every char in str
cout << c << endl; // print the current character followed by a newline

3.3标准类型 vector

C++语言既有类模板,也有函数模板,其中vector是属于一个类模板!!我开始以为vector是类似数组的一种结构,其实是一种模板。模板本省不是类或者函数,相反可以将模板看作为编译器生成的类或者函数编写的一份说明。编译器根据模板创建的类或者函数的这个过程称为实例化。  vector能容纳大多数的类型的对象作为其元素,但是引用不是对象所以不存在包含引用的vector。

    需要注意的是,C++早期的版本中如果vector的元素还是vector(或者其他类型),则定义的形式与现在的C++11新标准略有不同。过去,必须在外层的vector对象右尖括号和其他元素之间类型添加个空格,如应该写成vector<vector<int> >,而非vector<vector<int>>。某些编译器可能仍需要以老式的声明语句来处理元素为vector的vector对象。

     和任何一种类型一样,vector模板控制着定义和初始化向量的方法。下表所示:

在某些情况下,初始化的真实含义依赖于传递初始值用的花括号和圆括号。用一个整数初始化vector,可以是该vector的值也可以是该vector的数量。但是要想区分值和数量,在此用花括号和圆括号来进行区分:

vector<int> v1(10); // v1 has ten elements with value 0
vector<int> v2{10}; // v2 has one element with value 10
vector<int> v3(10, 1); // v3 has ten elements with value 1
vector<int> v4{10, 1}; // v4 has two elements with values 10 and 1

另一方面,如果初始时使用了花括号的形式但是提供的值又不能来列表初始化,就要考虑用这样的值来构造vector对象了。例如:

vector<string> v5{"hi"}; // list initialization: v5 has one element
vector<string> v6("hi"); // error: can't construct a vector from a str literal
vector<string> v7{10}; // v7 has ten default-initialized elements
vector<string> v8{10, "hi"}; // v8 has ten elements with value "hi"

尽管上面的例子除了第二条语句之外都用了花括号,但其实只有V5列表初始化。要想列表初始化vector对象,花括号的值必须与元素类型相同。

通常来讲,如果初始值不是连续相同的 ,一般先建立一个空的vector,向vector添加元素,放在尾端,利用push_back操作。注意:C++11中没有push_front,但是Qt是有的~~~。另外下表还列出了vector支持的常用操作:

访问vector中元素和string对象中一样,也可以通过索引值来进行访问。但是,添加vector元素,万万不能通过索引直接赋值进行添加。这个是灾难性的~~~~~~~正确的方法还是用push_back。

vector<int> ivec; // empty vector
for (decltype(ivec.size()) ix = 0; ix != 10; ++ix)
ivec[ix] = ix; // disaster: ivec has no elements

3.4迭代器介绍  iterator

      迭代器之前对我来说很陌生,因为C语言是没有的,迭代器类似于指针类型,也提供了对对象的间间访问。就迭代器而言,其对象是容器中的元素或者string对象中的字符。和指针不一样的是,获取迭代器不是使用取地址符,有迭代器的类型同时拥有返回迭代器的成员。比如,这些类型都拥有了名为begin和end的成员,其中begin成员负责返回指向第一个元素(或第一个字符)的迭代器。

// the compiler determines the type of b and e; see § 2.5.2 (p. 68)
// b denotes the first element and e denotes one past the last element in v
auto b = v.begin(), e = v.end(); // b and e have the same type

end成员负责返回指向容器“尾元素的下一个位置”的迭代器,也就是说,改迭代器指示的是容器的一个本不存在的“尾后” 元素。这样的迭代器没有什么实际含义,仅仅是一个标记而已。end成员返回的迭代器常被称作"尾后迭代器"。上面这几句话理解很困难,真的,请看下图:

这个和begin是完全不一样的!!!!!但是,如果容器为空,则begin和end返回的同一个迭代器,都是为尾后迭代器。迭代器也可以支持一些运算,标准迭代器运算符见下表:

另外迭代器也有类型。eg:

vector<int>::iterator it; // it can read and write vector<int> elements
string::iterator it2; // it2 can read and write characters in a string
vector<int>::const_iterator it3; // it3 can read but not write elements
string::const_iterator it4; // it4 can read but not write  characters

cosnt_iterator和常量指针很像,能读取但是不能修改它所指向的元素值,相反,iterator的对象可读可写。如果vector对象和string对象本身就是常量,只能使用cosnt_iterator。为了便于专门得到const_iterator类型的返回值,C++11新标准引入了两个新函数,分别是cbegin(),cend()。

vector<int> v;
const vector<int> cv;
auto it1 = v.begin(); // it1 has type vector<int>::iterator
auto it2 = cv.begin(); // it2 has type vector<int>::const_iterator

auto it3 = v.cbegin(); // it3 has type vector<int>::const_iterator

迭代器可以结合解引用获得所指的对象,例如对于一个字符串组成的vector对象来说,要想检查其元素是否为空,令it是该vector对象的迭代器,只需要检查it所指向字符串是否空就可以了,

(*it).empty();

(*it).empty() // dereferences it and calls the member empty on the resulting object
*it.empty() // error: attempts to fetch the member named empty from it  but it is an //iterator and has no member named empty

注意:上式园括号必不可少。该涉及到表达式的问题。

如果在循环体内使用了类似push_back等可以改变vector容量 的操作,这样会使该迭代器失效!!!

迭代器也可以进行递增运算符合递减运算符,这些都被称为迭代器运算符,见下图:

另外,只要两个迭代器指向的是同一个容器中的元素或者元素的下一个位置,就能将其相减,所得的结果是两个迭代器的距离。所谓的距离是右侧迭代器向前移动多少位置就能追向左侧迭代器,其类型名称为:difference_type的带符号整型数。

3.5数组

数组是C语言比较熟悉的一种数据结构,但是在性能和灵活性上不如vector。主要就是因为数组的大小是确定不变的。下面是数组初始化的一般形式:

unsigned cnt = 42; // not a constant expression
constexpr unsigned sz = 42; // constant expression
// constexpr see § 2.4.4 (p. 66)
int arr[10]; // array of ten ints
int *parr[sz]; // array of 42 pointers to int
string bad[cnt]; // error: cnt is not a constant expression
string strs[get_size()]; // ok if get_size is constexpr, error otherwise

当然,也可以显示初始化数组,即可以列表初始化数组,此时允许忽略数组的维度。如果声明时候没有指明数组的维度,编译器会根据初始值的数量计算并推测出来。如果指明了维度,初始值总数量小于等于维度数值。如果维度提供的比初始值数量大,则初始化靠前的元素,剩下的元素初始化默认值。

const unsigned sz = 3;
int ia1[sz] = {0,1,2}; // array of three ints with values 0, 1, 2
int a2[] = {0, 1, 2}; // an array of dimension 3
int a3[5] = {0, 1, 2}; // equivalent to a3[] = {0, 1, 2, 0, 0}
string a4[3] = {"hi", "bye"}; // same as a4[] = {"hi", "bye", ""}
int a5[2] = {0,1,2}; // error: too many initializers

字符数组有一种额外的初始化方式,我们可以用字符串字面值对此类进行数组初始化,当使用这种方式时一定注意字符串字面值结尾处还有一个空字符,这个空字符也会拷贝到字符数组中:

char a1[] = {'C', '+', '+'}; // list initialization, no null
char a2[] = {'C', '+', '+', '\0'}; // list initialization, explicit null
char a3[] = "C++"; // null terminator added
automatically
const char a4[6] = "Daniel"; // error: no space for the null!

数组是不允许拷贝和赋值的,但是~,有些编译器可以支持数组的赋值,这就是所谓的编译器扩展,但是一般来说最好避免使用非标准特性。

数组 有的声明非常复杂,特别是定义数组的指针和数组的引用,eg:

int *ptrs[10]; // ptrs is an array of ten pointers to int
int &refs[10] = /* ? */; // error: no arrays of references
int (*Parray)[10] = &arr; // Parray points to an array of ten ints
int (&arrRef)[10] = arr; // arrRef refers to an array of ten ints

默认情况下,类型修饰符从右向左依次绑定,对于ptrs来说,从右向左(参见2.3.3 节,第52页)理解其含义比较简单:首先知道我们定义的是一个天小为10的数组,它的名子是ptrs,然后知道数组中存放的是指向int的指针。但是对于Parray来说,从右向左理解就不太合理了。因为数组的维度是紧跟着被声明的名字的,所以就数组而言,由内向外阅读要比从右向左好多了。由内向外的顺序可帮助我们更好地理解Parray 的含义:首先是圆括号括起来的部分,*Parray 意味看Parray是个指针,接下来观察右边,可知道Parray是个指向大小为10的数组的指针,最后观察左边,知道数组中的元素是int。这样最终的含义就明白无误了,Parray是一个指针,它指向一个int数组,数组中包含10个元素。同理,(&arrRef)表示 arrRef是一个引用,它引用的对象是一个大小为(10的数组,数组中元素的类型是int 当然,对修饰符的数量并没有特殊限制:

int *(&arry) [10]  = ptrs; 

按照由内向外的顺序阅读上述语句,首先知道arry是一个引用,然后观察右边知道,arry引用的对象是一个大小为10的数组,最后观察左边知道,数组的元素类型是指向int的指针。这样,arry就是一个含有10个int型指针的数组的引用。要想理解数组声明的含义,最好的办法是从数组的名字开始按照由内向外的顺序阅读。
     在C++语言中,指针和数组有非常紧密的联系。就如即将介绍的,使用数组的时候编
器一般会把它转换成指针,通常情况下,使用取地址符(见232节,第4页)来获取指向某个的指针取地址符可以用于任何对象。数组的元素也是对象,对数组使用下标运算符得到该数组指定位置的元素。因此像其他对象一样,对数组的元素使用取地址符就能得到指向该元素的指针:

string nums[] = {"one", "two", "three"}; // array of strings
string *p = &nums[0]; // p points to the first element in nums

然而,数组还有一个特性:在很多用到数组名字的地方,编译器都会自动掉将其替为一个指向数组首元素的指针。
在大多数表达式中,使用数组类型的对象其实是使用一个指向该数组首元素的指针,由上可知,在一些情况下数组的操作实际上是指针的操作,这一结论有很多隐含的意思。其中一层意思是当使用数组作为一个auto(参见2.52节,第61页)变量的初始值时,推断得到的类型是指针而非数组:

int ia[] = {0,1,2,3,4,5,6,7,8,9}; // ia is an array of ten ints
auto ia2(ia); // ia2 is an int* that points to the first element in ia
ia2 = 42; // error: ia2 is a pointer, and we can't assign an int to a pointer

尽管ia是由10个整数构成的数组,但当使用⊥a作为初始值时,编译器实际执行的初始化过程类似手下面的形式:

auto ia2(&ia[0]); // now it's clear that ia2 has type int*

必须指出的是,当使用 decltype关键字(参见253节,第62页)时上述转换不会发生,decltype(ia)返回的类型是10个整数构成的数组:

decltype(ia) ia3 = {0,1,2,3,4,5,6,7,8,9};
ia3 = p; // error: can't assign an int* to an array
ia3[4] = i; // ok: assigns the value of i to an element in ia3


指针也是迭代器,但是在使用指针迭代数组过程容易出错。为了让指针的使用更简单、更安全C++11新标准入了两个名为begin和end的函数。这两个函数与容器中的两个同名成员函数类似,不过数组毕竟不是类类型,因此这两个函数不是成员函数。正确的使用形式是将数组作为它们的参数:

int ia[] = {0,1,2,3,4,5,6,7,8,9}; // ia is an array of ten ints
int *beg = begin(ia); // pointer to the first element in ia
int *last = end(ia); // pointer one past the last element in ia

和迭代器一样,两个指针相互减去得到的结果是他们之间的距离,参与运算的两个指针必须指向同一个数组的元素:

auto n = end(arr) - begin(arr); // n is 5, the number of elements in arr

和迭代器一样,两个指针相减的结果是一种名为ptrdiff_t的标准库类型,带符号的~

 

      尽管C++支持C风格字符串,但在C++程序中最好还是不要使用它们。这是因为C风格字符串不仅使用起来不太方便,而且极易引发程序漏洞,是诸多安全问题的根本原因。
      字符串字面值是一种通用结构的实例,这种结构即是C++由C继承而来的C区格字符串(C-style character string)。C风格字符串不是一种类型,而是为了表达和使用字符串而形成种约定俗成的写法。按此习惯书写的字符串存放在字符数组中并以空字符结束
null terminated).以空字符结束的意思是在字符串最后一个字符后面跟着一个空字符(‘\0’)。
    C 标准库 String函数如下:


上表列举了C语言标准库提供的一组函数,这些函数可用于操作C风格字符串,它们定义在 cstring头文件中, cstring是C语言头文件 string. h的C++版本。上述函数的指针必须指向空字符结束的数组:

char ca[] = {'C', '+', '+'}; // not null terminated
cout << strlen(ca) << endl; // disaster: ca isn't null terminated

比如上面的程序,回引发灾难性错误~~~。

为了兼容旧代码,可以混用string对象和C风格字符串:1允许使用空字符结尾的字符数组来初始string对象或者为string对象赋值。2 在string对象的加法运算中心允许使用以空字符结束的字符数组作为其中一个运算对象(不能两个都是)。但是上述性质反过来就不成立了,如果程序中需要一个C风格字符串,无法直接使用string对象来初始化它。eg:

char *str = s; // error: can't initialize a char* from a string
const char *str = s.c_str(); // ok

但是可以使用c_str()函数,它的返回值是一个C风格字符串。

当然,可以使用数组来初始化vector对象:

int int_arr[] = {0, 1, 2, 3, 4, 5};
// ivec has six elements; each is a copy of the corresponding element in int_arr
vector<int> ivec(begin(int_arr), end(int_arr));

3.6多维数组

严格的来说,C++数组并没有多维数组,通常所说的多维数组就是数组的数组。对于二维数组来说,常常把第一个维度叫做行,第二个维度叫做列。

假设row是一个二维数组,因为要改变数组元素的值,所以我们选用引用类型作为循环控制变量,但其实还有一个深层次的原因促使我们这么做。举一个例子,考虑如下的循环:

for (const auto &row : ia) // for every element in the outer array
    for (auto col : row) // for every element in the inner array
    cout << col << endl;

    对于内层数组的每一个元素这个循环中并没有任何写操作,可是我们还是将外层循环的控制变量声明成了引用类型。假设不用引用类型,则如下:

for (auto row : ia)
    for (auto col : row)

   这时程序无法编译。具体原因是因为初始化row的时候会自动将这些数组形式的元素转换成指向数组内首元素的指针。这样得到的row类型就是int*,显然内层就不合法了。

     当定义指向多维数组的数组的指针时,这个多维数组的数组实际上是数组的数组。所以,多维数组的名字转换得到的是实际指针指向第一个内层数组的指针~~

好的,关于第三章重要容易出错的知识点就总结到这里~~~如果需要后续修改~~

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值