类简介
结构回顾
public和private权限修饰符
类简介
c++中结构和类极其类似,区别有两点:
-
默认成员变量以及成员函数的访问级别
- c++结构内部的成员变量以及成员函数,默认的访问级别都是public
- c++类内部的成员变量以及成员函数,默认的访问基本都是private
-
继承的默认访问级别
- c++结构体继承默认的是public,而c++类的继承默认都是private
类的组织与书写规范
- 类的声明:放在一个
.h文件(头文件)中,头文件可以跟类名相同。 - 类的具体实现:放在一个
.cpp文件(源文件)中。源文件要include对应的头文件。
声明和定义的区别
- 声明:用于向程序表明变量或函数的类型和名字。
- 定义:用于为变量和函数分配存储空间,还可为变量指定初始值。在一个程序中,变量或函数只能定义一次,却可以声明多次。(定义也是声明:当定义变量时我们声明了它的类型和名字)。
C++中头文件(.h)和源文件(.cpp)都应该写些什么
总结下来就是头文件写的就是类的声明(包括类里面的成员和方法的声明)和函数的声明,但一般来说不写出具体的实现。对应的同名(可以不同名,但出于编程习惯最好同名)源文件来写出具体的实现,当然这个实现的源文件里必须要include对应的头文件,这样能保证声明和定义对应实现完整性,这是因为编译器在编译代码时,只会去编译.cpp格式的源文件,并且预编译器会递归的把.cpp所有#include的头文件都“拷贝”到.cpp文件中去。然后main文件或者其他文件如果想要调用实体时(比如变量或函数)只用include相应的头文件,因为变量在使用前就要被定义或者声明,这里即指只用声明即可,因为编译器会在链接时去找对应的声明的实现。
写头文件在开头和结尾处加上预编译语句#ifndef #define endif的作用
# pragma once 、#ifndef/#define使用区别
C++ 在.h文件中包含头文件和在.cpp文件中包含头文件有什么区别?
编译器如何根据头文件来找到相应实现的cpp文件
编译器不管头文件的,所以头文件和源文件取名不一样也没关系,头文件只是用来被cpp文件包含的,被包含之后,它就成了那个cpp文件的一部分了,而编译器只编译.cpp文件,不会去单独编译一个头文件的。编译器这样做之后,针对每个编译过的cpp文件生成一个obj文件。然后链接器把所有这些obj文件链接成一个程序或者是exe或dll(或做成静态的lib)。如果在链接的过程中,有些实体(比如变量或函数)找不到定义,则会报link错误,这还没完,链接的时候也会检查,比如某个函数f在不同的cpp文件中出现了多次定义,而且都是外链接,那链接器也会报重复定义地错误。
对于模板,声明和定义都要写在一起放在头文件。
函数新特性、内联函数、const详解
函数后置返回类型
函数定义中,行参如果在函数体内用不到的话,则可以不给定形参变量名字,只给其类型。
函数声明时,可以只给形参类型,没有形参名。
// 函数声明
void func_1(int, int);
// 函数定义
int func_2(int, int) {
return 0;
}
把函数返回类型放到函数名字之前,这种写法,叫前置返回类型。
c++11中,后置返回类型,就是在函数声明和定义中,把返回类型放在参数列表之后。(前面放auto,表示函数返回类型放到参数列表之后,而放在参数列表之后的返回类型是通过 -> 开始的)
// 函数声明
auto func_3(int a, int b) -> void;
// 函数定义
auto func_4(int a, int b) -> void {
return;
}
内联函数
在函数定义前增加关键字inline,该函数会变成内联函数。对于函数体很小,调用很频繁的函数,可以尝试引入inline(内联函数)。inline影响编译器,在编译阶段对inline这种函数进行处理,系统尝试将调用该函数的动作替换为函数本体,通过这种方式,来提升性能。
// 函数定义前加上inline,让这个函数成为内联函数
inline int func(int a) {
return 0;
}
inline只是开发者对编译器的一个建议,编译器可以尝试去做,也可以不去做,这取决于编译器的诊断功能,也就是说,决定权在编译器。
内联函数的定义必须放在头文件,这样需要用到这个内联函数的.cpp文件都能够通过#include把这个内联函数的源代码include进来,以便找到这个函数的本地源代码并尝试将该函数的调佣替换为函数体内的语句。
缺点:会带来代码膨胀的问题,所以内联函数函数体尽量要小。
注意:各种编译器对include的处理各不相同,inline函数尽量简单,代码尽可能少。循环、分支、递归调用尽量不要出现在inline函数中,否则的话,编译器很可能会拒绝让这个函数成为内联函数。
函数杂合用法总结
- 函数返回类型为void,表示函数不返回任何类型,但是我们可以调用一个【返回类型是void的函数】,让它作为另一个【返回类型是void的函数】的返回值。
void func_b() {
return;
}
void func_a() {
return func_b();
}
- 没有形参可以保持形参列表为空,或者int func(void)
- 如果一个函数不调用的话,则该函数可以只有声明部分,没有定义部分。
- 普通函数,定义只能定义一次(定义放在.cpp文件中)声明可以声明多次。一般函数定义.cpp文件会include自己的函数声明文件(.h)。
- void func(int a, int b) 在c++中,更习惯用引用类型的形参来取代指针类型的形参。提倡在c++中,多使用引用类型的形参。
- c++中,函数运行同名,但是形参列表的参数类型或者数量应该有明显的区别。
const char*、char const*、char* const三者区别
const char *p; // p指向的东西不能通过p来修改(p指向的目标,那个目标中的内容不能通过p来改变) char const *p 等价于 const char *p
char * const p = “Hello World”; // 定义的时候必须初始化,p一旦指向了一个东西之后,就不可以再指向其他东西了。 *p = 'y'; // 但可以修改p指向的目标中的内容 const char * const p = str; // p的指向也不能改变,p指向的内容也不能通过p来改变。
int &b = 31; // 错误 const int &b = 31; // 分配了内存
函数形参中带const
把形参写成const的形式有很多好处:
- 可以防止你无意中修改了形参值导致实参值被无意修改。
- 实参类型可以更灵活。(形参带const时,实参可以带const也可以不带。形参不带const时,实参一定不能带const)
string类型介绍
string类型简介
c++标准库中的类型,代表一个可变长字符串。string这个类型,看成一个类类型。
c语言中用法char str[100] = "hello world!"
定义和初始化string对象
#include <iostream>
using namespace std;
int main() {
// 默认初始化, s1 = "", ""空串,表示里边没有字符。
string s1;
// 把"hello world"这个字符串内容拷贝到了s2代表的一段内存中
// 拷贝时不包括末尾的\0。
string s2 = "hello world";
// 跟s2写法效果一样
string s3("hello world");
// 把s2中的内容拷贝到了s4所代表的一段内存中
string s4 = s2;
// 这也是一种初始化string的方式。用的是c语言形式的字符数组(字符串)来初始化string。
char str[100];
string s11(str);
// "aaaaaa",把s5初始化为连续num个字符'a'组成的字符串,
// // 这种方式不太推荐,因为会在系统内部创建临时对象。
int num = 6;
string s5(num, 'a');
}
string对象上的操作
# include <iostream>
# include <string>
using namespace std;
int main() {
// b) size() / length():返回字节 / 字符数量,代表该字符串的长度, unsigned int
string s_b1;
cout << s_b1.size() << endl; // 0
cout << s_b1.length() << endl; // 0
string s_b2 = "程序员";
cout << s_b2.size() << endl; // 9
cout << s_b2.length() << endl; // 9
string s_b3 = "hello world!";
cout << s_b3.size() << endl; // 12
cout << s_b3.length() << endl; // 12
// c) s[n] 返回s中第n个字符(n是个整型值,n代表的是一个位置,位置从0开始,到s.size() - 1)
// 如果用下标超过这个范围的内容,或者本来人家一个空字符串,也用s[n]去访问,都会产生不可预测的结果。
// d) s1+s2: 字符串的连接,返回连接后的结果,其实就是得到一个新的string对象。
string sd1 = "abcd";
string sd2 = "hijk";
string sd3 = sd1 + sd2;
cout << sd3 << endl; // "abcdhijk"
// e) s1 = s2 字符串对象赋值,用s2里边的内容取代原来s1里的内容
string se1 = "abcd";
string se2 = "de";
se1 = se2;
cout << se1 << endl; // de
// f) s1 == s2 判断两个字符串是否相等,大小写敏感,也就是大写字符和小写字符是两个不同的字符
// 相等: 长度相同,字符全相同
string sf9 = "abc";
string sf10 = "abC";
if (sf9 == sf10) {
cout << "sf9 == sf10" << endl;
}
// g) s1 != s2 判断两个字符串是否不相等。
string sg1 = "abc";
string sg2 = "abC";
if (sg1 == sg2) {
cout << "sg1 != sg2" << endl;
}
// h) s.c_str(): 返回一个字符串s中的内容指针,返回一个指向正规的c字符串的指针常量,也就是以\0结尾的
// 这个函数的引入是为了与c语言兼容,在c语言中没有string类型,
// 所以我们得通过string类对象的c_str()成员函数把string对象转换成c语言中的字符串样式。
// i) 读写string对象:
string si1;
// 从键盘输入
// cin >> si1;
// cout << si1 << endl;
// j) 字面值和string相加
string sj1 = "abc";
string sj2 = "defg";
string sj3 = sj1 + "and" + sj2 + "e";
cout << sj3 << endl; // abcanddefge
//string sj5 = "abc" + "def"; //语法上不允许这么加,编译器不知道应该把"abc"转换成什么格式
//string sj5 = "abc" + sj1 + "def"; // 中间夹一个string对象,语法上就允许。
//string sj5 = "abc" + "def + sj2; // 错误,看来两个字符串不能挨着相加,否则会语法报错。
}
vector类型介绍
vector类型简介
标准库:集合或者动态数组,可以把若干对象放在里面。vector能把其他对象装进来,也被称为容器。
vector<int> v; // 表示这个vector里面存的是int类型数据(int型对象)
<int>: 类模板; vector本身就是一个类模板,<int>实际上就是类模板的实例化的过程。
vector当成类类型(残缺的类类型)
vector<int>:在vector之后加一对<>,<>内部放上类型信息(完整的类类型)
不能往vector里装引用
vector<int&> v1; // 引用是个别名,不是对象,不是对象不能往vector里面装。
定义和初始化vector对象
# include <iostream>
# include <vector>
# include <vector>
using namespace std;
int main() {
// a) 空vector
// 创建一个string类型的空的vector对象(容器),目前这个mystr不包含任何元素
vector<string> mystr;
mystr.push_back("abc");
mystr.push_back("def");
// b) 元素拷贝的初始化方式,mystr2和mystr3与mystr中元素的地址不同。
vector<string> mystr2(mystr); // 把mystr里面的元素拷贝给了mystr2
vector<string> mystr3(mystr);// 把mystr里面的元素拷贝给了mystr3
// c) c++11标准中,用列表初始化方法,值用{}括起来
vector<string> mystr4 = {"aaa", "bbb", "ccc"}
// d) 创建指定数量的元素,有元素数量概念的东西,一般会用圆括号
vector<int> v1(15, -200); // 创建15个int类型的元素,每个元素的值是-200
vector<string> v2(5, "hello"); // 创建5个string类型的元素,每个元素的值都是hello
vector<int> v3(20); // 20个元素,[0]...[19] 每个元素值都是0
vector<string> v4(5); // 5个元素,[0]...[4]每个元素的值为""
// e) 多种初始化方式,()一般表示对象中元素数量,{}一般表示元素内容,但不绝对
vector<int> i1(10); // 表示10个元素,每个元素值都是缺省的0
vector<int> i2{10}; // 表示是一个元素,该元素的值为10
vector<string> i3{"hello"}; // 一个元素,内容是hello
vector<int> i4(10, 1); // 10个元素,每个元素内容都是1
vector<int> i5 {10, 1}; // 2个元素,第一个是10,第二个是1,等同于初始化列表
// 下面两种写法过于怪异,不要这么写
vector<string> i6 {10}; // 10个元素,每个都是""
vector<string> i7 {10, "hello"}; // 10个元素,每个元素内容都是"hello"
// 要想正常的通过{}进行初始化,那么{}里面的值的类型,必须跟vector后面这个<>里面元素类型相同。
}
vector对象上的操作
# include <iostream>
# include <vector>
# include <vector>
using namespace std;
int main() {
// vector对象上的操作:最常用的是不知道vector里有多少个元素,需要动态增加/减少
// 所以一般来讲,先创建空vector
// vector很多操作与string类似
// a) 判断是否为空empty(),返回的是bool
vector<int> ivec;
if (ivec.empty()) {
cout << "ivec is empty" << endl;
}
// push_back:常用方法,用于向vector中的末尾增加一个元素
ivec.push_back(1);
ivec.push_back(2);
for (int i = 3; i <= 100; i++) {
ivec.push_back(i);
}
// c) size : 返回元素个数
cout << ivec.size() << endl;
// d) clear: 移除所有元素,将容器清空
ivec.clear();
// e) v[n]: 返回v中第n个元素(n是个整数值)代表位置,位置从0开始,必须小于size()a,
// 如果引用的下标超过这个范围,或者用[]访问了一个空vector,那么就会产生不可预料的结果,编译器发现不了。
// f): = 赋值
vector<int> ivec2;
vector<int> ivec3(100, 1);
ivec3.push_back(111);
// ivec2得到了100个元素,ivec2原来的元素被冲掉了
ivec2 = ivec3;
// 用{}中的值取代了ivec2原有值
ivec2 = {12, 13, 14, 15};
// g):==, !=: 相等,不相等
// 两个vector相等: 元素数量相同,对应位置的元素值也得一样,否则就是不相等。
// h) 范围for的应用
vector<int> vec1{1, 2, 3, 4, 5};
for (auto &item: vec1) {
item *= 2;
}
for (auto &item: vec1) {
cout << item << endl;
}
}
范围for进一步讲解
# include <iostream>
# include <vector>
# include <vector>
using namespace std;
int main() {
// h) 范围for的应用
vector<int> vec1{1, 2, 3, 4, 5};
for (auto &item: vec1) {
item *= 2;
}
for (auto &item: vec1) {
cout << item << endl;
}
// 范围for进一步讲解
for (auto &item: vec1) {
// 导致输出彻底乱套,因为第一次进入for循环时系统会在内部记录vec1结束的位置
// for循环过程中一旦遇到结束位置,就结束遍历。在vector中插入和删除元素会导致
// vec1的结束位置发生改变,结束位置的值一改变,会导致for循环输出混乱。
cout << item << endl;
}
// 结论:在for语句中(遍历一个容器等类似操作中),千万不要改动vector容器的容量,增加/删除都不可以。
}
第九节:迭代器精彩演绎、失效分析及弥补、实战
迭代器简介
迭代器是一种遍历容器内元素的数据类型,这种数据类型感觉有点像指针,我们理解的时候就理解为迭代器用来指向容器中的某个元素。对于string、vector等,可以用[]来遍历元素,但我们很少用[],更常用的方式是迭代器(更通用)。通过迭代器,我们就可以读容器中的元素值,读string中的每个字符,还可以修改某个迭代器所指向的元素值。
容器的迭代器类型
vector<int> iv = {100, 200, 300};
vector<int>::iterator iter; // 定义迭代器,也必须是vector<int>
大家在理解的时候,就可以把整个vector<int>::iterator理解成一个类型,这种类型就专门用于迭代器。当我们用这个类型定义一个变量时,这个变量就是迭代器,这里iter这个变量就是个迭代器。
迭代器begin()/end()操作,反向迭代器rbegin()/rend()操作
begin()/end()用来返回迭代类型。rbegin()/rend()用类返回迭代类型。
vector<int> iv = {100, 200, 300};
vector<int>::iterator iter; // 定义迭代器,也必须是vector<int>
iter = iv.begin();
iter = iv.end();
- 如果容器中有元素,则
begin()返回的迭代器,指向容器中的第一个元素。相当于iter指向了iv[0]。 end()返回的迭代器指向的并不是末端元素。而是末端元素的后边,这个末端元素的后边可以立即为end()指向一个并不存在的元素。如下图所示:
- 如果一个容器为空,那么begin()和end()返回的迭代器相同。
vector<int> iv2;
vector<int>::iterator iter_begin = iv2.begin();
vector<int>::iterator iter_end = iv2.end();
if (iter_begin == iter_end) {
cout << "容器为空" <<endl;
}
反向迭代器
反向迭代器(逆向迭代器)用的是rbegin()/rend()。
- rbegin():返回一个反向迭代器,指向反向迭代器的第一个元素。
- rend():返回一个反向迭代器,指向反向迭代器的最后一个元素的下一个位置。
// 正向遍历元素
vector<int> iv = {100, 200, 300};
for(vector<int>::iterator iter = iv.begin(); iter != iv.end(); iter++) {
cout << *iter <<endl; // 100, 200, 300
}
// 反向遍历元素
for(vector<int>::reverse_iterator iter = iv.rbegin(); iter != iv.rend(); iter++) {
cout << *iter <<endl; // 300, 200, 100
}
迭代器运算符
*iter:返回迭代器iter所指向元素的引用。必须要保证这个迭代器指向的是有效的容器元素,不能指向end(),因为end()是末端元素的后边。也就是end()指向的是一个不存在的元素。++iter和iter++:让迭代器指向容器的下一个元素,已经指向end()的时候不能再++。--iter和iter--:让迭代器指向容器中的上一个元素,已经指向开头元素时不能再--。- 用
iter1 == iter2和iter1 != iter2来判断两个迭代器是否相等,如果两个迭代器指向同一个元素,就相等,否则就不等。 - 引用结构中的成员:
(*iter).成员或者iter->成员
const_iterator迭代器
const_iterator迭代器:表示值不能改变的意思,这里的值不能改变表示这个迭代器指向的元素值不能改变,而不是表示这个迭代器本身不能改变,也就是说,迭代器本身是可以不断指向下一个元素。所以只能从容器中读数据,不能通过这个迭代器改写容器中的元素。感觉起来更像常量指针。
vector<int> iv = {100, 200, 300};
vector<int>::const_iterator iter
for(iter = iv.begin(); iter != iv.end(); iter++) {
*iter = 4; // 报错
cout << *iter <<endl; // 100, 200, 300
}
cbegin()和cend()操作
c++11引入的两个新函数cbegin()和cend(),跟begin、end类似,cbegin()和cend()返回的都是常量迭代器。
vector<int> iv = {100, 200, 300};
for(auto iter = iv.cbegin(); iter != iv.cend(); iter++) {
*iter = 4; // 报错,不能给常量赋值。这说明cbegin返回的都是常量迭代器
cout << *iter <<endl; // 100, 200, 300
}
迭代器失效
在操作迭代器过程中(使用了迭代器这种循环体),千万不要改变vector容器的容量,也就是不要增加或者删除vector容器中的元素。
往容器中增加或者从容器中删除元素,这些操作可能会使指向容器元素的指针、引用、迭代器失效。
失效就表示不能再代表任何容器中的元素。一旦使用失效的东西,就等于犯了严重的程序错误,很多情况下程序会直接崩溃。
vector容器常用操作与内存释放
struct conf {
char name[40];
char content[40];
};
int main {
conf* pconf1 = new conf;
strcpy_s(pconf1->name, sizeof(pconf1->name), "service_name");
strcpy_s(pconf1->content, sizeof(pconf1->content), "1区");
conf* pconf2 = new conf;
strcpy_s(pconf2->name, sizeof(pconf2->name), "service_id");
strcpy_s(pconf2->content, sizeof(pconf2->content), "100000");
vector<conf *> conf_list;
conf_list.push_back(pconf1); // [0]
conf_list.push_back(pconf2); // [1]
// 我们要释放内存,自己new的就要自己释放,否则会造成内存泄漏
vector<conf *>:: iterator pos;
for(pos = conf_list.begin(); pos != conf_list.end(); pos++) {
delete (*pos); // 删除的是vector对应位置的元素,但没有破坏迭代器。删除的是下图中蓝色框中的内容
}
conf_list.clear(); // 干掉vecotr,要不要都行,不写的话系统也会自动清空。干掉的是下图中红色框中的内容。
return 0;
}
第十节:类型转换
隐式类型转换
隐式类型转换:系统自动进行。
int m = 3 + 45.6; // 把小数部分截掉,也属于隐式类型转换的一种行为。
double n = 3 + 45.6
显式类型转换(强制类型转换)
// int k = 5 % 3.2; // 语法错
// int k = 5 % (int)3.2; // c语言风格强制类型转换
// int k = 5 % int(3.2); // 函数风格的强制类型转换(c语言风格的强制类型转换)
c++强制类型转换分为4种:我们现在写程序就应该使用c++风格的强制类型转换。 这4种强制类型转换,分别用于不同的目的,每一个都有不同的名字。提供4种的目的:提供更丰富的含义和功能,更好的类型检查机制,方便代码的书写和维护。
下面4种强制类型转换都被称呼为:命名的强制类型转换(因为他们每一个都有一个名字并且名字各不相同)
使用强制类型转换通用格式:
强制类型转换名<type>(express);
强制类型转换名:static_cast、dynamic_cast、const_cast、reinterpret_cast
type:转换的目标类型
express:你要转换的值
static_cast
静态转换,大家就可以理解为“正常转换”,编译的时候就会进行类型转换的检查。
- 代码中要保证转换的安全性和正确性,static_cast含义跟c语言中的强制类型转换差不多。
- c语言风格的强制类型转换,以及编译器能够进行的隐式类型转换,都可以用
static_cast显示完成。
可用
- 可用于相关类型转换,比如整型和实型之间的转换
double f = 100.34f;
int i = (int) f; // c风格
int i2 = static_cast<int>(f); // c++风格的类型转换
- 子类转换成父类
class A {};
class B : public A {};
B b;
A a = static_cast<B>(b); // 把子类转成父类的对象
// A a1
// b = static_cast<A>(a1); // 报错,父类不能转成子类
void *与其他类型指针之间的转换,void *是无类型指针,可以指向任何指针类型(万能指针)
int i = 10;
int *p = &i;
void *q = static_cast<void *>(p); //int *转成void *
int *db = static_cast<int *>(q); // 原来是int,所以可以转回int
不可用
- 一般不能用于指针类型之间的转换,比如:
int *转double *,float *转double *等。
double f = 100.0f;
double *pf = &f;
int *i = static_cast<int *>(pf); // 不可以
float *fd = static_cast<float *>(pf); // 不可以
dynamic_cast
主要用于运行时类型识别和检查。主要用来父类型和子类型之间转换用的(父类型指针指向子类型对象,然后用dynamic_cast把父类型指针往子类型指针转)。
后续在第三章第十节详细讲解。
const_cast
去掉指针或者引用的const属性,该转换能够将const性质转换掉。编译时就会进行类型转换。
const int a = 90;
int a1 = const_cast<int>(a); // a不是指针也不是引用,不能转
const int *ap = &a;
int *ap1 = const_cast<int *>(ap); // 语法正确
*ap2 = 120; // 这种写值行为,是一种未定义行为,不要这么做。
reinterpret_cast
编译时就会进行类型转换的检查。
reinterpret:重新编译。(将操作数内容解释为另一种不同的类型【能把操作数的类型都转了】)
可以处理无关类型的转换。也就是两个转换内容之间没什么关系。就等于可以乱转,自由转。怎么转都行,很随意。被认为是危险的类型转换,因为怎么转都行,编译器都不报错。
常用于如下两种转换:
- 将一个整型(地址)转换为指针,一种类型指针转换为另一种类型指针,按照转换后的内容重新解释内存中的内容。
- 也可以从一个指针类型转换为一个整型。
int i = 10;
int *pi = &i;
int *p2 = reinterpret_cast<int *>(&i);
char *p3 = reinterpret_cast<char *>(pi);
long long num = 8898899400; // 8字节的, 16进制:0x2 126A 6DC8
int *p = reinterpret_cast<int *>(num); // 0x 126A 6DC8 把前边的2丢了,因为p是4字节
long long ne = reinterpret_cast<long long>(p); // 指针类型转整型 = 208964808 = 0x126A 6DC8
reinterpret只要合乎规则的用而不乱用,其实很好用。
总结
- 强制类型转换,不建议使用。强制类型转换能够抑制编译器报错。
- 学习目的:认识这些类型转换符,方便阅读代码。
- 资料说:reinterpret_cast危险,使用const_cast意味着设计缺陷。
- 如果实在需要使用类型转换,用c++风格类型转换。
- 一般static_cast和reinterpret_cast能够很好的取代c风格类型转换。