目录
接上篇。我说过,这种基础类文章适合有过c++开发的基础的童鞋阅读,旨在帮助他们重温这个久别的“瑞士军刀”。
模板
模板是泛型编程的基础,分为函数模板和类模板。
函数模板
函数模板能够用来创建一个通用的函数。以支持多种不同的形參。避免重载函数的函数体反复设计。
template <typename T>
T max(T a,T b)
{
// 函数的主体
return a > b ? a : b;
}
//代替了
int max(int a,int b)
int max(float a,float b)
类模板(泛型类)
为类定义一种模式。使得类中的某些数据成员、默写成员函数的參数、某些成员函数的返回值,能够取随意类型;常见的 容器比如 向量 vector <int> 或 vector <string> 就是模板类。
template<class E,class T>
class Queue {
public:
T add(E e,T t){
return e+t;
}
};
Queue<int,float> q;
q.add(1,1.1f) = 2.1f
拷贝构造函数和赋值运算符
在默认情况下(用户没有定义,但是也没有显式的删除),编译器会自动的隐式生成一个拷贝构造函数和赋值运算符。但用户可以使用delete
来指定不生成拷贝构造函数和赋值运算符,这样的对象就不能通过值传递,也不能进行赋值运算。
class Person
{
public:
Person(const Person& p) = delete;
Person& operator=(const Person& p) = delete;
private:
int age;
string name;
};
上面的定义的类Person显式的删除了拷贝构造函数和赋值运算符,在需要调用拷贝构造函数或者赋值运算符的地方,会提示_无法调用该函数,它是已删除的函数_。
还有一点需要注意的是,拷贝构造函数必须以引用的方式传递参数。这是因为,在值传递的方式传递给一个函数的时候,会调用拷贝构造函数生成函数的实参。如果拷贝构造函数的参数仍然是以值的方式,就会无限循环的调用下去,直到函数的栈溢出。
他们何时调用
调用的是拷贝构造函数还是赋值运算符,主要是看是否有新的对象实例产生。如果产生了新的对象实例,那调用的就是拷贝构造函数;如果没有,那就是对已有的对象赋值,调用的是赋值运算符。
调用拷贝构造函数主要有以下场景:
1.对象作为函数的参数,以值传递的方式传给函数;
2.对象作为函数的返回值,以值的方式从函数返回;
3.使用一个对象给另一个对象初始化。
class Person
{
public:
Person(){}
Person(const Person& p)
{
cout << "Copy Constructor" << endl;
}
Person& operator=(const Person& p)
{
cout << "Assign" << endl;
return *this;
}
private:
int age;
string name;
};
void f(Person p)
{
return;
}
Person f1()
{
Person p;
return p;
}
int main()
{
Person p;
Person p1 = p; // 1
Person p2;
p2 = p; // 2
f(p2); // 3
p2 = f1(); // 4
Person p3 = f1(); // 5
getchar();
return 0;
}
执行结果如下:
分析如下:
1.这是虽然使用了"=",但是实际上使用对象p来创建一个新的对象p1。也就是产生了新的对象,所以调用的是拷贝构造函数。
2.首先声明一个对象p2,然后使用赋值运算符"=",将p的值复制给p2,显然是调用赋值运算符,为一个已经存在的对象赋值 。
3.以值传递的方式将对象p2传入函数f内,调用拷贝构造函数构建一个函数f可用的实参。
4.这条语句拷贝构造函数和赋值运算符都调用了。函数f1以值的方式返回一个Person对象,在返回时会调用拷贝构造函数创建一个临时对象tmp作为返回值;返回后调用赋值运算符将临时对象tmp赋值给p2.
5.按照4的解释,应该是首先调用拷贝构造函数创建临时对象;然后再调用拷贝构造函数使用刚才创建的临时对象创建新的对象p3,也就是会调用两次拷贝构造函数。不过,编译器也没有那么傻,应该是直接调用拷贝构造函数使用返回值创建了对象p3。
深拷贝、浅拷贝
任何一个对象对该值的修改都会影响到另一个对象,这种情况就是浅拷贝。深拷贝和浅拷贝主要是针对类中的指针和动态分配的空间来说的,因为对于指针只是简单的值复制并不能分割开两个对象的关联,任何一个对象对该指针的操作都会影响到另一个对象。这时候就需要提供自定义的深拷贝的拷贝构造函数,消除这种影响。通常的原则是:
含有指针类型的成员或者有动态分配内存的成员都应该提供自定义的拷贝构造函数;在提供拷贝构造函数的同时,还应该考虑实现自定义的赋值运算符。
对于拷贝构造函数的实现要确保以下几点:
对于值类型的成员进行值复制;
对于指针和动态分配的空间,在拷贝中应重新分配分配空间;
对于基类,要调用基类合适的拷贝方法,完成基类的拷贝。
容器
容器,就是用来存放东西的盒子。
常用的数据结构包括:数组array, 链表list, 树tree, 栈stack, 队列queue, 散列表hash table, 集合set、映射表map 等等。容器便是容纳这些数据结构的。这些数据结构分为序列式与关联式两种,容器也分为序列式容器和关联式容器。
STL 标准模板库,核心包括容器、算法、迭代器。
序列式容器/顺序容器
元素排列次序与元素无关,由元素添加到容器的顺序决定 。
容器 | 说明 |
---|---|
vector | 支持快速随机访问 |
list | 支持快速插入、删除 |
deque | 双端队列 允许两端都可以进行入队和出队操作的队列 |
stack | 后进先出LIFO(Last In First Out)堆栈 |
queue | 先进先出FIFO(First Input First Output)队列 |
priority_queue | 有优先级管理的queue |
向量(vector):连续存储的元素;
列表 (list):由节点组成的双向链表,每个结点包含着一个元素;
双端队列(deque):连续存储的指向不同元素的指针所组成的数组;
以上三种容器操作基本一样:
#include <vector>
using namespace std;
vector<int> vec_1;
//1个元素
vector<int> vec_2(1);
//6个值为 1 的元素
vector<int> vec_3(6,1);
//使用容器初始化
vector<int> vec_4(vec_3);
//通过下标操作元素
int i = vec_3[1];
int j = vec_3.at(1);
//首尾元素
vec_3.front()
vec_3.back()
//插入元素
//vector不支持 push_front list,deque可以
vec_1.push_back(1);
//删除元素 vector不支持 pop_front
vec_1.pop_back();
//释放
//可以单个清除,也可以清除一段区间里的元素
vec_3.erase(vec_3.begin(),vec_3.end())
//清理容器 即erase所有
vec_3.clear();
//容量大小
vec_3.capacity();
//在容器中,其内存占用的空间是只增不减的,
//clear释放元素,却不能减小vector占用的内存
//所以可以对vector 收缩到合适的大小
vector< int >().swap(vec_3);
//在vec是全局变量时候
//建立临时vector temp对象,swap调用之后对象vec占用的空间就等于默认构造的对象的大小
//temp就具有vec的大小,而temp随即就会被析构,从而其占用的空间也被释放。
迭代器:
//获得指向首元素的迭代器 模板类,不是指针,当做指针来使用
vector<int>::iterator it = vec.begin();
//遍历元素
for (; it < vec.end(); it++)
{
cout << *it << endl;
}
//begin和end 分别获得 指向容器第一个元素和最后一个元素下一个位置的迭代器
//rbegin和rend 分别获得 指向容器最后一个元素和第一个元素前一个位置的迭代器
//注意循环中操作元素对迭代器的影响
vector<int>::iterator it = vec.begin();
for (; it < vec.end(); )
{
//删除值为2的元素
if (*it == 2) {
it = val.erase(it); //返回下一个有效的迭代器,无需+1
}
else {
it++;
}
}
栈(stack):后进先出的值的排列。
stack<int> s;
//入栈
s.push(1);
s.push(2);
//弹栈
s.pop();
//栈顶
cout << s.top() << endl;
队列(queue):后进先出的值的排列。
stack<int> s;
//入栈
s.push(1);
s.push(2);
//弹栈
s.pop();
//栈顶
cout << s.top() << endl;
队列(queue) :先进先出的值的排列 。
queue<int> q;
q.push(1);
q.push(2);
//移除最后一个
q.pop();
//获得第一个
q.front();
//最后一个元素
cout << q.back() << endl;
优先队列(priority_queue ):元素的次序是由所存储的数据的某个值排列的一种队列。
//最大的在队首
priority_queue<int>;
//在vector之上实现的
priority_queue<int, vector<int>, less<int> >;
//vector 承载底层数据结构堆的容器
//less 表示数字大的优先级高,而 greater 表示数字小的优先级高
//less 让优先队列总是把最大的元素放在队首
//greater 让优先队列总是把最小的元素放在队首
//less和greater都是一个模板结构体 也可以自定义
class Student {
public:
int grade;
Student(int grade):grade(grade) {
}
};
struct cmp {
bool operator ()(Student* s1, Student* s2) {
// > 从小到大
// < 从大到小
return s1->grade > s2->grade;
}
bool operator ()(Student s1, Student s2) {
return s1.grade > s2.grade;
}
};
priority_queue<Student*, vector<Student*>, cmp > q1;
q1.push(new Student(2));
q1.push(new Student(1));
q1.push(new Student(3));
cout << q1.top()->grade << endl;
关联式容器
关联容器和大部分顺序容器操作一致;关联容器中的元素是按关键字来保存和访问的 支持高效的关键字查找与访问。
集合(set):由节点组成的红黑树,每个节点都包含着一个元素,元素不可重复。
set<string> a;
set<string> a1={"fengxin","666"};
a.insert("fengxin"); // 插入一个元素
a.erase("123"); //删除
键值对(map):由{键,值}对组成的集合。
map<int, string> m;
map<int, string> m1 = { { 1,"Lance" },{ 2,"David" } };
//插入元素
m1.insert({ 3,"Jett" });
//pair=键值对
pair<int, string> p(4, "dongnao");
m1.insert(p);
//insetrt 返回 map<int, string>::iterator : bool 键值对
//如果 插入已经存在的 key,则会插入失败
//multimap:允许重复key
//使用m1[3] = "xx" 能够覆盖
//通过【key】操作元素
m1[5] = "yihan";
cout << m1[5].c_str() << endl;
//通过key查找元素
map<int, string>::iterator it = m1.find(3);
cout << (*it).second.c_str()<< endl;
// 删除
m1.erase(5);
//遍历
for (it = m1.begin(); it != m1.end(); it++)
{
pair<int, string> item = *it;
cout << item.first << ":" << item.second.c_str() << endl;
}
//其他map================================
unordered_map c++11取代hash_map(哈希表实现,无序);哈希表实现查找速度会比RB树实现快,但rb整体更节省内存;需要无序容器,高频快速查找删除,数据量较大用unordered_map;需要有序容器,查找删除频率稳定,在意内存时用map。
类型转换
c++除了能使用c语言的强制类型转换外,还新增了四种强制类型转换:static_cast、dynamic_cast、const_cast、reinterpret_cast。语法为:
static_cast<new_type> (expression)
dynamic_cast<new_type> (expression)
const_cast<new_type> (expression)
reinterpret_cast<new_type> (expression)
注:new_type为目标数据类型,expression为原始数据类型变量或者表达式。
const_cast
用于修改类型的const或volatile属性。 除了const 或volatile修饰之外, new_type和expression的类型是一样的。
const char *a;
char *b = const_cast<char*>(a);//去掉const指针const属性
char *a;
const char *b = const_cast<const char*>(a);//给指针加上const属性
const int g = 20;
int *h = const_cast<int*>(&g);//去掉const常量const属性
const int g = 20;
int &h = const_cast<int &>(g);//去掉const引用const属性
const char *g = "hello";
char *h = const_cast<char *>(g);//去掉const指针const属性
static_cast
相当于传统的C语言里的强制转换,该运算符把expression转换为new_type类型,用来强迫隐式转换,例如non-const对象转为const对象,编译时检查,用于非多态的转换,可以转换指针及其他,但没有运行时类型检查来保证转换的安全性。
-
基本数据类型之间互转。如:float转成int、int转成unsigned int等
-
把指针转换成void类型指针 。如:float*转成void*、Bean*转成void*、函数指针转成void*等
-
子类指针/引用与 父类指针/引用 转换。
-
注意:static_cast不能转换掉expression的const、volatile、或者__unaligned属性。
class Parent {
public:
void test() {
cout << "p" << endl;
}
};
class Child :public Parent{
public:
void test() {
cout << "c" << endl;
}
};
Parent *p = new Parent;
Child *c = static_cast<Child*>(p);
//输出c
c->test();
//Parent test加上 virtual 输出 p
int e = 10;
const int f = static_cast<const int>(e);//正确,将int型数据转换成const int型数据
const int g = 20;
int *h = static_cast<int*>(&g);//编译错误,static_cast不能转换掉g的const属性
reinterpret_cast
new_type必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以把一个指针(new_type)转换成一个整数,也可以把一个整数转换成一个指针(new_type)(先把一个指针转换成一个整数,再把该整数转换成原类型的指针,还可以得到原先的指针值)。
错误的使用reinterpret_cast很容易导致程序的不安全,只有将转换后的类型值转换回到其原始类型,这样才是正确使用reinterpret_cast方式。
int output(int p){
cout << p <<endl;
return 0;
}
typedef int (*test_func)(int );//定义函数指针test_func
int main(){
int p = 10;
test_func fun1 = output;
fun1(p);//正确
test_func fun2 = reinterpret_cast<test_func>(&p);
fun2(p);//...处有未经处理的异常: 0xC0000005: Access violation
float i = 10;
//&i float指针,指向一个地址,转换为int类型,j就是这个地址
int j = reinterpret_cast<int>(&i);
cout << hex << &i << endl;
cout << hex << j << endl;
return 0;
}
他们的区别
static_cast和reinterpret_cast的区别主要在于多重继承,比如:
class A {
public:
int m_a;
};
class B {
public:
int m_b;
};
class C : public A, public B {};
C c;
printf("%p, %p, %p", &c, reinterpret_cast<B*>(&c), static_cast <B*>(&c));
前两个的输出值是相同的,最后一个则会在原基础上偏移4个字节,这是因为static_cast计算了父子类指针转换的偏移量,并将之转换到正确的地址(c里面有m_a,m_b,转换为B*指针后指到m_b处),而reinterpret_cast却不会做这一层转换。
dynamic_cast
运算形式如下:
dynamic_cast<type*>(e)
dynamic_cast<type&>(e)
dynamic_cast<type&&>(e)
type必须是一个类类型,在第一种形式中,type必须是一个有效的指针,在第二种形式中,type必须是一个左值,在第三种形式中,type必须是一个右值。在上面所有形式中,e的类型必须符合以下三个条件中的任何一个:e的类型是是目标类型type的公有派生类、e的类型是目标type的共有基类或者e的类型就是目标type的的类型。如果一条dynamic_cast语句的转换目标是指针类型并且失败了,则结果为0。如果转换目标是引用类型并且失败了,则dynamic_cast运算符将抛出一个std::bad_cast异常(该异常定义在typeinfo标准库头文件中)。e也可以是一个空指针,结果是所需类型的空指针。
在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的。
//基类至少有一个虚函数
//对指针转换失败的得到NULL,对引用失败 抛出bad_cast异常
Parent *p = new Parent;
Child *c = dynamic_cast<Child*>(p);
if (!c) {
cout << "转换失败" << endl;
}
Parent *p = new Child;
Child *c = dynamic_cast<Child*>(p);
if (c) {
cout << "转换成功" << endl;
}
值得注意的是,我们可以在一个操作中同时完成类型转换和条件检查两项任务,如下代码:
//指针类型
if(Child *c = dynamic_cast<Child*>(p);){
//使用c指向的Child对象
}
else{
//使用p指向的Parent对象
}
//引用类型
void f(const Parent &b){
try{
const Child &d = dynamic_cast<const Parent &>(b);
//使用b引用的Child 对象
}
catch(std::bad_cast){
//处理类型转换失败的情况
}
}
注:因为不存在所谓空引用,所以引用类型的dynamic_cast转换与指针类型不同,在引用转换失败时,会抛出std::bad_cast异常,该异常定义在头文件typeinfo中。
char*与int转换
//char* 转int float
int i = atoi("1");
float f = atof("1.1f");
cout << i << endl;
cout << f << endl;
//int 转 char*
char c[10];
//10进制
itoa(100, c,10);
cout << c << endl;
//int 转 char*
char c1[10];
sprintf(c1, "%d", 100);
cout << c1 << endl;
异常
void test1()
{
throw "测试!";
}
void test2()
{
throw exception("测试");
}
try {
test1();
}
catch (const char *m) {
cout << m << endl;
}
try {
test2();
}
catch (exception &e) {
cout << e.what() << endl;
}
//自定义
class MyException : public exception
{
public:
virtual char const* what() const
{
return "myexception";
}
};
//随便抛出一个对象都可以
文件与流操作
模式 | 描述 |
---|---|
r | 打开一个已有的文本文件,允许读取文件。 |
w | 打开一个文本文件,允许写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会从文件的开头写入内容。如果文件存在,则该会被截断为零长度,重新写入。 |
a | 打开一个文本文件,以追加模式写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会在已有的文件内容中追加内容。 |
r+ | 打开一个文本文件,允许读写文件。 |
w+ | 打开一个文本文件,允许读写文件。如果文件已存在,则文件会被截断为零长度,如果文件不存在,则会创建一个新文件。 |
a+ | 打开一个文本文件,允许读写文件。如果文件不存在,则会创建一个新文件。读取会从文件的开头开始,写入则只能是追加模式。 |
/**C 语言的文件读写操作
* 头文件:stdio.h
* 函数原型:FILE * fopen(const char * path, const char * mode);
* path: 操作的文件路径
* mode:模式
**/
FILE *f = fopen("xxxx\\t.txt","w");
//写入单个字符
fputc('a', f);
fclose(f);
FILE *f = fopen("xxxx\\t.txt","w");
char *txt = "123456";
//写入以 null 结尾的字符数组
fputs(txt, f);
//格式化并输出
fprintf(f,"%s",txt);
fclose(f);
//========================================================================
fgetc(f); //读取一个字符
char buff[255];
FILE *f = fopen("xxxx\\t.txt", "r");
//读取 遇到第一个空格字符停止
fscanf(f, "%s", buff);
printf("1: %s\n", buff);
//最大读取 255-1 个字符
fgets(buff, 255, f);
printf("2: %s\n", buff);
fclose(f);
//二进制 I/O 函数
size_t fread(void *ptr, size_t size_of_elements,
size_t number_of_elements, FILE *a_file);
size_t fwrite(const void *ptr, size_t size_of_elements,
size_t number_of_elements, FILE *a_file);
//1、写入/读取数据缓存区
//2、每个数据项的大小
//3、多少个数据项
//4、流
//如:图片、视频等以二进制操作:
//写入buffer 有 1024个字节
fwrite(buffer,1024,1,f);
数据类型 | 描述 |
---|---|
ofstream | 输出文件流,创建文件并向文件写入信息。 |
ifstream | 输入文件流,从文件读取信息。 |
fstream | 文件流,且同时具有 ofstream 和 ifstream 两种功能。 |
//C++ 文件读写操作
//<iostream> 和 <fstream>
char data[100];
// 以写模式打开文件
ofstream outfile;
outfile.open("XXX\\f.txt");
cout << "输入你的名字: ";
//cin 接收终端的输入
cin >> data;
// 向文件写入用户输入的数据
outfile << data << endl;
// 关闭打开的文件
outfile.close();
// 以读模式打开文件
ifstream infile;
infile.open("XXX\\f.txt");
cout << "读取文件" << endl;
infile >> data;
cout << data << endl;
// 关闭
infile.close();
你的打赏是我奋笔疾书的动力!
支付宝打赏:
微信打赏: