目录
C++基本语言
结构、权限修饰符与类简介
结构
结构是一种自定义的数据类型。
C++中的结构除了具备C中结构的所有功能外,还增加许多扩展功能,其中最突出的扩展功能之一就是C++的结构不仅仅有成员变量,还可以在其中定义成员函数。
public 和 private 权限修饰符
在结构和类中,有三个重要的权限修饰符,分别是public(公有)、private(私有)、protected(保护)。
- public:用这个修饰符修饰的成员(成员变量、成员函数),可以被外界访问。
- private:用这修饰符修饰的成员,只能被该结构或类内部定义的成员函数使用。
对于用struct 定义的结构来讲,默认的情况下,所有的成员变量和成员函数都是public的。对于public的成员变量和成员函数,可以直接通过对象来引用,而private则不行。
类简介
类也是一种用户自定义的数据类型。
结构和类的区别:
- C语言中并没有类的概念,C++中才有类的概念。
- 结构用struct定义,类用class定义
- C++中的结构和类极其类似,主要区别如下:
- C++中结构体内部成员变量及成员函数默认的访问级别是public,而C++中类的内部成员变量及成员函数的默认访问级别是private。
- C++中结构体的继承默认是public,而C++中类的继承默认是private。
在书写C++程序的时候,无论代码想实现一个什么样的功能,都应该设法通过写一个或多个类来达到这个目的,因为C++语言中最核心的部件就是类。
类的组织
在开发一个大型项目的时候,很可能会写多个类去实现各种功能,显然,如此多的类,在书写的时候肯定要有一个规范。
一般来讲,类的定义代码放到一个.h
头文件中,头文件的主文件名可以和类名相同。
类的具体实现代码(一般都是成员函数的实现代码)放到一个(或多个).cpp
文件(源程序文件)中,这个.cpp
文件的主文件名一般也和类名相同。
如果有任何其他.cpp
文件想使用这个类的时候,就在其文件的头部使用#include
命令把这个类定义相关的.h
头文件包含进来。
函数新特性、inline内联函数与const详解
后置返回类型
在C++11中引入了一种新的语法,叫做后置返回类型,也就是在函数声明或定义中把返回类型写在参数列表之后,对于有一些返回类型比较复杂的情形,这种写法可能更容易让人看懂。同时,有一些特殊场合,还必须采用这种语法来书写。
auto func(int, int) -> int; // 函数声明中的后置返回类型写法
auto func(int a, int b) -> int // 函数定义中的后置返回类型写法
{
return 1;
}
前面放置auto关键字,表示函数返回类型放到参数列表之后。
编写函数时,一个函数内包含的代码不要太长,不同的功能尽量分解到多个函数中去写,一般一个函数内建议包含几十到上百行代码,尽量不要书写上千行代码,函数体太过冗长也增加了他人阅读这段代码的理解难度和时间。
inline内联函数
调用函数是要消耗系统资源的,尤其是一些函数体很小但却频繁调用的函数,调用起来很不划算,因为要频繁地进行压栈、出栈动作以处理函数调用和返回的问题,这也意味着要频繁地为它们开辟内存。
为了解决这种函数体很小、调用又很频繁的函数所耗费的系统性能问题,引入了inline关键字。
inline int myfunc(int testv)
{// 函数定义之前加了inline关键字,叫作内联函数。
return testv * (5 + 4) * testv;
}
inline关键字的效果如下:
- 影响编译器,在编译阶段完成对inline函数的处理,系统尝试将调用该函数的动作替换为函数的本体(不再进行函数调用),以提升程序执行性能。
- inline关键字只是程序员对编译器的一个建议,编译器可以尝试去做,也可以不去做,这取决于编译器的诊断功能,决定权在编译器,无法人为控制。
- 内联函数的定义放在头文件中,这样需要用到这个内联函数的
.cpp
文件都能通过#include
来包含这个内联函数的源代码,以便找到这个函数的本体(源代码)并尝试将对该函数的调用替换为函数体内的语句。
使用内联函数的优缺点:
用函数本体取代函数调用,显然可以增加效率。但同时带来的问题是会导致代码膨胀。代码在程序运行时也是要占用内存的,因为内存中有代码段专门保存程序代码。所以内联函数体要尽可能短小、简单。
注意:
- 编译器不同,可能内联的结果也不同。
- inline函数尽量简单,代码尽量少,尤其是各种复杂的循环、分治、递归调用等,尽量少出现在内联函数中。
- 内联函数有点像宏展开(
#define
),不过宏展开和内联函数有各种差别,如类型检查等。 - 可以把
constexpr
函数看成是更严格的一种内联函数,因为constexpr
自带inline属性。
函数特殊写法的总结
-
函数返回类型为void表示函数不返回任何类型。但是可以调用一个返回类型为void的函数,让它作为另一个返回类型为void的函数的返回值。
void funca() { // ... } void funcb() { return funca(); }
-
不带形参的函数定义也可以写成如下形式,或者形参列表直接空着更好
int myfunc(void) {return 1;}
-
如果一个函数不调用,则该函数可以只有声明部分,没有定义部分。
-
函数声明一般放在头文件中,函数定义一般放在源文件中,所以函数只能定义一次,但可以声明多次,因为多个源文件可能都包含一个头文件,而且习惯上,函数定义的源文件中也把函数声明的头文件包含进来。
-
函数可以同名,叫做函数重载。
const char *、char const * 与 char * const三者的区别
-
const char* p
表示p指向的内容不能通过p来修改,p称为常量指针。
char str[] = "I Love China!"; const char* p = str; // 定义的时候必须初始化 p++; // p可以指向不同的位置,只要这些位置的内存归我们管即可 *p = 'Y'; // 但不可以修改指向的目标中的内容
-
char const* p
等价于const char* p
-
char* const p
,p不可以再指向其他内容,p称为指针常量。char str[] = "I Love China!"; char* const p = str; // 定义的时候必须初始化 p++; // 不可以,p指向一个内容后,不可以再指向其他内容 *p = 'Y'; // 但可以修改指向的目标中的内容
-
const char* const p = str;
或char const* const p = str
表示p的指向不能改变,并且p指向的内容不能通过p来改变。
函数形参中带const
- 在引用传递中,可以防止无意中修改了形参值导致实参值被无意中修改掉。
- 实参类型可以更加灵活。
struct student { int num; };
void fs(student& stu){}
int main()
{
student abc;
abc.num = 100;
const student& def = abc;
fs(def); // 错误,因为def类型时const&,而函数fs的形参不带const
return 0;
}
const student&
这种类型可以接受的实参类型更多样化(程序代码更灵活),可以接收普通引用作为实参,也可以接收常量引用作为实参。
string类型
定义和初始化string对象
string类型是一个标准库中的类型,代表一个可变长字符串,string位于std命名空间中。
string s1; // 默认初始化,结果是s1 = "",代表一个空串,里面没有字符。
string s2 = "I love China!"; // 把I love China!这个字符串内容复制到了s2代表的一段内存中
string s3("I love China!"); // 跟s2的写法效果一样
string s4 = s2; // 把s2中的内容复制到了s4所代表的一段内存中
int num = 6;
string s6(num, 'a'); // 把s5初始化为连续num个字符'a'组成的字符串,不过这种方式系统内部会创建临时对象,不太推荐
string对象上的常用操作
-
判断是否为空empty(),返回布尔值。
-
size()或length():返回字节/字符数量,可以理解成返回的是一个无符号数
unsigned int
。 -
s[n]:返回s中的第n个字符(n是一个整型值),字符位置从0开始计算,位置值n必须小于size()。
-
s1 + s2:字符串连接,返回连接后的结果(得到一个新的string对象)。
-
s1 = s2:字符串对象赋值,用s2里面的内容取代原来s1里面的内容。
-
s1 == s2:判断两个字符串是否相等(长度相等,字符也全相同),大小写敏感。
-
s1 != s2:判断两个字符串是否不相等。
-
s.c_str()
:返回一个字符串s中的内容指针(string字符串里面的内容),返回的是一个指向正规C字符串的常量指针,所以是以'\0'
结尾的。这个函数是为了与C语言兼容,在C语言中没有string类型,所以得通过string类对象的成员函数
c_str()
把string对象转换成C中的字符串样式。string s9 = "abc"; string s10 = "abC"; const char* p = s10.c_str(); // 现在p指向s10里边的字符串了,可以跟踪查看 char str[100]; strcpy_s(str, sizeof(str), p); // 字符串内容复制到str中 cout << str << endl; // abC
-
字面值和string相加。
string s1 = "abc"; string s2 = "defg"; string s3 = s1 + " and " + s2 + 'e'; cout << s3 << endl; // abc and defge
隐式类型转换,实际上在这里"and" 和 'e’都被转换成了string对象参与加法运算。但是要注意:
string s1 = "abc"; string s2 = "defg"; string s3 = "abc" + "def"; // 错误,语法上不允许 string s3 = "abc" + s1 + "def"; // 中间夹杂一个string对象,语法上就允许 string s3 = "abc" + "def" + s2; // 错误,看来两个字符串不能挨着相加,否则会报语法错误
-
范围for针对string的使用。范围for语句,能够遍历一个序列中的每个元素,这里string就可以看成是一个字符序列。
string s1 = "I Love China!"; for (auto c : s1) { cout << c << endl; }
上面c的类型其实就是char。
vector类型
vector类型是一个标准库中的类型,代表一个容器、集合或者动态数组这样一种概念。也就是说可以把若干类型相同的对象放到vector里面。
定义和初始化vector对象
- 空vector
vector<string> mystr; // 创建一个string类型的空vector对象(容器)
其实vector就是一个类模板,这里的"<>"实际上是类模板的一个实例化过程。
创建空的容器之后,后续可以用相关操作函数往这个空对象里增加数据,例如可以使用vector的成员函数push_back
往容器末尾增加数据。
mystr.push_back("abcd");
-
在vector对象元素类型相同的情况下,进行vector对象元素复制。
vector<string> mystr2(mystr); // 把mystr元素复制给了mystr2 vector<string> mystr3 = mystr; // 把mystr元素复制给了mystr3
-
在C++11中,还可以用初始化列表方法给初值,这个时候用"{}"括起来。
vector<string> def = { "aaa", "bbb", "ccc" };
-
创建指定数量的元素。
vector<int> ijihe(15, -200); // 创建了15个int类型元素的集合,每个元素值都是-200 vector<string> sjihe(5, "hello"); // 创建5个hello
vector对象上的操作
在使用vector时,最常见的情况是并不知道vector里面会有多少个元素,使用时会根据需要动态增加和减少。
所以一般来讲,是先创建一个空的vector对象,然后通过代码向这个vector里面增加或减少元素。
-
判断是否为空empty(),返回布尔值。
-
push_back:用于向vector末尾增加一个元素。
-
size:返回元素个数。
-
clear:移除所有元素,将容器清空。
-
v[n]:返回v中的第n个元素。
-
赋值运算符 =
vector<int> ivec; // 先声明成空的vector对象 ivec.push_back(1); ivec.push_back(2); vector<int> ivec2; ivec2.push_back(111); ivec2 = ivec; // 得到2个元素,用ivec中的内容取代了ivec2中原有内容 ivec2 = { 12,13,14,15 }; // 用{}中的值取代了ivec2原有值 cout << ivec2.size() << endl; // 4
-
相等和不等(==和!=)。两个vector对象相等:元素数量相同,对应位置的元素值也相同。
-
范围for的应用:和string对范围for的应用类似。
vector<int> vecvalue{ 1,2,3,4,5 }; for (auto& vecitem : vecvalue) // 为了修改vecvalue内部值,这里是引用,引用会绑定到元素上,达到修改元素值的目的 vecitem *= 2; for (auto vecitem : vecvalue) cout << vecitem << endl;
如果在范围for中,增加改变vector容量的代码,则输出就会变得混乱:
vector<int> vecvalue{ 1,2,3,4,5 }; for (auto vecitem : vecvalue) { vecvalue.push_back(888); // 导致输出彻底混乱 cout << vecitem << endl; }
因为每执行for循环,都会重新定义vecitem,并且把它的值初始化成vecvalue序列中的下一个值。在刚刚进入这个for循环时,在系统内部会记录序列结束的位置值,但一旦在这个范围for里面改动这个序列的容量(如增加/删除元素),那么这个序列结束的位置值就肯定会发生改变,这个改变导致for语句的混乱,其输出的值也就乱了。
迭代器精彩演绎、失效分析及弥补、实战
迭代器简介
迭代器是一种遍历容器内元素的数据类型。
string和vector都可以通过"[]"(下标)访问里面的字符/元素,但实际在C++中,很少通过下标来访问它们,一般都是采用迭代器来访问。有很多容器不支持"[]"操作,但容器都支持迭代器操作。
string其实是字符串,不是容器,但string也支持用迭代器遍历。
通过迭代器,可以读取容器中的元素值、修改容器中某个迭代器所代表(所指向)的元素值。此外,迭代器可以像指针一样,通过++、–等运算符从容器中的一个元素移动到另一个元素。
容器的迭代器类型
C++标准库为每种容器都定义了对应的迭代器类型。以vector为例:
vector<int> iv = { 100,200,300 }; // 定义一个容器
vector<int>::iterator iter; // 定义迭代器,也必须是以vector<int>开头
iterator是每个容器里面都定义了的一个成员(类型名)。
迭代器begin/end、反向迭代器rbegin/rend操作
迭代器
每一种容器,如vector,都定义了一个叫begin的成员函数和一个叫end的成员函数。这两个成员函数正好用来返回迭代器类型。
-
begin返回一个迭代器类型。
iter = iv.begin(); // 如果容器中有元素,则begin返回的迭代器指向的是容器中的第一个元素 // 这里相当于iter代表着元素v[0]
-
end返回一个迭代器类型。
iter = iv.begin(); // 如果容器中有元素,则begin返回的迭代器指向的是容器中的第一个元素
end返回的迭代器并不指向容器vector中的任何元素,它实际上起到一个标志作用,如果迭代器从容器的begin位置开始不断遍历,那么有一个时刻,
iter
走到了end位置时,代表已经遍历完了容器中的所有元素。 -
传统的通过迭代器访问容器中元素的方法:
// 经典传统用法,这里用++、!=等运算符来对迭代器进行操作 for (vector<int>::iterator iter = iv.begin(); iter != iv.end(); iter++) cout << *iter << endl;
反向迭代器
如果想从后往前遍历一个容器,那么,用反向迭代器就比较方便。反向迭代器使用的是rbegin
和rend
成员函数。
rbegin
返回一个反向迭代器类型,指向容器的最后一个元素。rend
返回一个反向迭代器类型,指向容器的第一个元素的前面位置。
for (vector<int>::reverse_iterator riter = iv.rbegin(); riter != iv.rend(); riter++)
cout << *riter << endl;
迭代器运算符
*iter
:返回迭代器iter
所指向元素的引用。++iter
:和iter++
功能相同,让迭代器指向容器中的下一个元素。但是已经指向end的迭代器,不能再++,否则运行时报错。--iter
:和iter--
功能相同,让迭代器指向容器中的前一个元素。iter1 == iter2
或iter1 != iter2
:判断两个迭代器是否相等。如果两个迭代器指向的是同一个元素,则相等,否则不等。
const_iterator 迭代器
除了iterator这种迭代器类型,每种容器还有另外一种迭代器类型,叫作const_iterator
,该迭代器指向的元素的值不能改变。
如果这个容器对象是一个常量,那么就必须使用const_iterator
,否则报错。
const vector<int> iv = { 100,200,300 }; // 定义一个容器,注意前面的const
vector<int>::const_iterator iter;
for (iter = iv.begin(); iter != iv.end(); ++iter)// 这里begin和end返回的是const_iterator
{// 返回的是iterator还是const_iterator,取决于这个容器对象是否是const对象
cout << *iter << endl;
}
C++11引入了两个新函数,cbegin
和cend
成员函数,与begin、end非常类似,但是,无论容器是否是常量容器,cbegin
、cend
返回的都是常量迭代器const_iterator
。
vector<int> iv = { 100,200,300 }; // 定义一个容器
for (auto iter = iv.cbegin(); iter != iv.cend(); ++iter)
{
*iter = 58; // 错误,不能给常量赋值,这说明cbegin返回的是常量迭代器
cout << *iter << endl;
}
迭代器失效
在操作迭代器的过程中(使用了迭代器的这种循环体),千万不要改变容器的容量。
vector<int>vecvalue{ 1,2,3,4,5 };
for (auto beg = vecvalue.begin(), end = vecvalue.end(); beg != end; ++beg)
{
// 这种循环内千万不要改变vecvalue对象的容量
}
对于向容器中添加元素和从容器中删除元素操作要小心,因为这些操作可能都会使指向容器元素的迭代器(也包括指针、引用等)失效。这种失效就表示它不能再代表任何容器中的元素,如果使用了这种失效的迭代器,就好比使用了没有被初始化的指针一样,很多情况下会导致程序崩溃。
不同的容器实现机理不同(例如有的容器内部数据使连续存储的,插入元素时一旦原有内存不够用,则可能就会导致容器中原有数据全部迁移到一个新内存去,如vector等容器),不同的插入操作、不同的插入位置,会导致迭代器、指针、引用部分或者全部失效,甚至在循环体中的诸如vecvalue.end()
代码都会因为插入数据操作导致失败。
另外一种情况是删除操作,如果从容器种删除一个元素,那么,当前指向这个被删除元素的迭代器、指针、引用肯定是立即失效,绝对不能再引用它们。不同的容器,针对删除操作,不同的删除位置,也会导致迭代器、指针、引用部分或全部失效,甚至在循环体中的诸如vecvalue.end()
代码都会因为删除数据操作导致失效。
解决方法是:如果在一个使用了迭代器的循环中插入元素到容器,那只插入一个元素后就应该跳出循环,不能再继续用这些迭代器操作容器。
类型转换:static_cast、reinterpret_cast等
隐式类型转换
一个double
和一个int
做加法运算,那么int
会转换成double
,然后再和double
做运算,这个类型转换是系统自动进行的,不需要人为介入,所以这种转换叫做隐式类型转换。
将一个浮点数赋值给整型变量,系统会把小数部分舍弃,把整数部分赋予这个变量,这也可以看作隐式类型转换。
显示类型转换(强制类型转换)
显示类型转换也叫强制类型转换。
int k = 5 % 3.2; // 语法错,系统要求 % 两侧必须都为整型
int k = 5 % (int)3.2; // 没问题
上面是C语言风格的强制类型转换,没有类型方面的检查,直接硬转,转的是对还是错,程序员必须提供保障。
在C++中,强制类型转换分为4种,分别用于不同的目的,C++种分这么细是为了提供更丰富的含义和功能以及更好的类型检查机制,方便代码的书写和维护。
通用形式:
强制类型转换名<type>(express);
其中强制类型转换名就是static_cast
、dynamic_cast
、const_cast
、reinterpret_cast
这4个名字之一,用来指定是哪种转换;type
是转换的目标类型;express
是要转换的值。
-
static_cast
:静态转换。属于编译的时候就会进行类型转换的检查,代码中要保证转换的安全性和正确性,与C语言中的强制类型转换的感觉差不多。一般的编译器能够执行的隐式的类型转换也都可以用static_cast
来显示完成。一般不能用于指针类型之间的转换,如int*
转double*
、float*
转double*
等。可用于:-
相关类型转换,如整型和实型之间的转换。
double f = 100.34f; int i = (int)f; // C风格的 int i2 = static_cast<int>(f); // C++风格的
-
继承关系中,子类转换成父类类型也能用
static_cast
。class A{}; // 父类 class B : public A {}; // 子类 int main() { B b; A a = static_cast<A>(b); // 把子类转换成父类可以,但父类转换成子类是不可以的 return 0; }
-
void*
与其他类型指针之间的转换:void*
属于“无类型指针”,也就是可以指向任何的指针类型。int i = 10; int* p = &i; void* q = static_cast<void*>(p); int* db = static_cast<int*>(q); // 原本就是int指针,所以还要转回int,否者可能转完后里面的值就是错的
-
-
dynamic_cast
:应用在运行时类型识别和检查方面,主要用来进行父类型转换成子类型。但是因为要做类型检查,所以检查的代价也很昂贵,但也保证了转换的安全性。 -
const_cast
:去除指针或者引用的const
属性,属于编译的时候就会进行类型转换的检查。const int ai = 90; int ai2 = const_cast<int>(ai); // ai不是指针,不是引用,所以不能转 const int* pai = &ai; int* pai2 = const_cast<int*>(pai); // 正确 *pai = 120; // 错误,未定义行为,因为这里实际上是const的
-
reinterpret_cast
:也属于编译的时候就会进行类型转换的检查。reinterpret
表示重新解释、重新解读的意思(将所操作的内容解释为另一种不同的类型),用来处理无关类型的转换,也就是两个转换的类型之间没有什么关系,即瞎转,怎么转都行,这个类型转换相当随意。常用于:-
将一个整型(地址)转换成指针、一种类型指针转换成另外一种类型指针,按照转换后的类型重新解释内存中的内容。
int i = 10; int* pi = &i; int* p2 = reinterpret_cast<int*>(&i); char* pc = reinterpret_cast<char*>(pi); // 这种转换本身不报错,但这个地址本身存的是int,却把它转换成字符指针,虽然转换成功,但要是当成字符指针用,肯定程序回出错。
-
也可以从一个指针类型到一个整型进行转换。
-
总结
- 强制类型转换一般不建议使用。因为会干扰系统的正常类型检查。很多异常转换本来编译器会报错的,但是一旦用了这些类型转换,就会抑制编译器的报错行为。
- 有句话叫使用
reinterpret_cast
非常危险,而使用const_cast
总是意味着设计缺陷。因为平白无故利用const_cast
去掉const属性是很让人费解的设计。 - 一般
static_cast
和reinterpret_cast
就能很好地取代C语言风格的类型转换。