C++高级教程(上)
# 文件和流
⚠️fstream 文件库
# 文件读写
#include <fstream> // 文件库
#include <iostream> // 流
using namespace std;
int main ()
{
char data[100];
// ofstream:以写模式打开文件
ofstream outfile;
outfile.open("test.txt");
cout << "请输入名称: ";
// 屏幕显示输入光标
cin.getline(data, 100); // getline():从外部读取一行信息
// 向文件写入用户输入的数据
outfile << data << endl;
cin.ignore(); // ignore() :忽略掉之前读语句留下的多余字符
// 关闭打开的文件
outfile.close();
// 以读模式打开文件
ifstream infile;
infile.open("test.txt");
// 输出内容到对象
infile >> data;
// 打印信息
cout << data << endl;
// 关闭打开的文件
infile.close();
return 0;
}
# 文件位置指针
// 定位到 fileObject 的第 n 个字节(假设是 ios::beg)
fileObject.seekg( n );
// 把文件的读指针从 fileObject 当前位置向后移 n 个字节
fileObject.seekg( n, ios::cur );
// 把文件的读指针从 fileObject 末尾往回移 n 个字节
fileObject.seekg( n, ios::end );
// 定位到 fileObject 的末尾
fileObject.seekg( 0, ios::end );
# 异常处理
异常处理函数:try、catch
抛出异常:throw
# 异常处理
异常处理涉及到三个关键字:try、catch、throw
#include <iostream>
using namespace std;
double division(int a, int b)
{
if( b == 0 )
{
throw "异常reason:分母不能为零!"; // 抛出的异常的类型
}
return (a/b);
}
int main ()
{
try {
cout << division(6, 0) << endl;
}catch (const char* msg) {
cerr << msg << endl; // 捕获一个 char 类型的异常
}catch( ExceptionName eN )
{
// 捕获一个类型为 **ExceptionName** 的异常
}
// 可以尝试罗列多个 catch 语句,用于捕获不同类型的异常。
return 0;
}
# 标准的异常
C++ 提供了一系列标准的异常
异常 | 描述 |
---|---|
1、std::exception | 该异常是所有标准 C++ 异常的父类。 |
std::bad_alloc | 该异常可以通过 new 抛出。 |
std::bad_cast | 该异常可以通过 dynamic_cast 抛出。 |
std::bad_exception | 这在处理 C++ 程序中无法预期的异常时非常有用。 |
std::bad_typeid | 该异常可以通过 typeid 抛出。 |
3、std::runtime_error | 理论上不可以通过读取代码来检测到的异常。 |
---|---|
std::overflow_error | 当发生数学上溢时,会抛出该异常。 |
std::range_error | 当尝试存储超出范围的值时,会抛出该异常。 |
std::underflow_error | 当发生数学下溢时,会抛出该异常。 |
# 自定义异常
可以通过继承和重载 exception 类来自定义新的异常
#include <iostream>
#include <exception>
using namespace std;
// 自定义用户异常类型
struct UserException : public exception
{
string reason;
// what() 是异常类提供的一个公共方法,可以被所有子类重载
const char * what () const throw ()
{
return "Exception:User"; // std::exception 默认 std::exception
}
};
void age_e(int m) {
if (m < 0)
{
struct UserException userExp;
userExp.reason = "年龄小于零";
throw userExp; // throw 抛出异常类型
}
}
int main()
{
try
{
age_e(-8); // 代码
age_e(-3); // 代码
}
catch(UserException& e) // 处理抛出的 UserException 异常
{
std::cout << "异常类型:" << e.what() << std::endl;
std::cout << "异常reason:" << e.reason << std::endl;
}
catch (const char* msg) {
cerr << msg << endl; // 捕获一个 char 类型的异常
}
catch(std::exception& e)
{
// 其他的错误
}
}
结果:⚠️ 同一时间的程序只有第一个异常抛出,然后程序进程被中断
异常类型:Exception:User
异常reason:年龄小于零
# 动态内存
1、申请内存:动态内存需要手动malloc() 分配(C++封装了new方法帮助申请内存的同时初始化创建的对象)
2、释放内存:用户自己申请的内存在生命周期结束时需要手动释放 delete
# iOS中内存
如果你是iOS开发者,那么应该很清楚理解这些内容:
早期OC在MRC时都默认用户手动 alloc-init 创建一个对象(或者直接new),然后用完需要手动释放 release (C++中等同于delete),当然局部对象使用autorelease 能自动释放。
再后来OC的ARC优化了手动释放对象内存的逻辑,通过自动释放池优化垃圾机制,通常用户不用在关心对象释放问题。
另外,在Swift中更愿意开发者更多的使用结构体这样明确对象内存大小的结构,从而运行效率更高,因为过多的动态申请会消耗一定性能。这算是一种空间换时间的
# iOS中混合开发
如果你在OC(ARC)中使用混合开发c/c++的话,那么对应非OC代码依然需要遵循想要内存管理机制。比如c代码依然需要手动释放内存。
# 动态内存
栈:局部变量一般占用栈内存,(我理解)部分对象实际数据可能是以地址引用方式并非都是存储在栈内存
**堆:**用户手动alloc申请的内存通常在堆区,遵循谁申请谁释放的原则
# 数据类型动态分配内存
#include <iostream>
using namespace std;
int main ()
{
int* age = NULL; // 初始化为空指针
age = new int; // 申请内存,通常4字节
*age = 18; // 内存地址存储值
cout << "年龄 : " << *age << endl;
delete age; // 释放申请的内存
return 0;
}
结果:
年龄 : 18
# 数组的动态内存分配
#include <iostream>
using namespace std;
int main ()
{
int* values = NULL; // 初始化空指针
values = new int[5]; // 申请内存
values[0] = 1;
values[1] = 4;
cout << "一维数组首地址:" << *values << endl;
delete [] values; // 释放内存
int row = 2;
int col = 3;
int **mvalues = new int* [row]; // 为行分配内存
// 为列分配内存(3列2行)
for(int i = 0; i < col; i++) {
mvalues[i] = new int[col];
}
mvalues[0][0] = 2;
mvalues[0][1] = 4;
cout << "二维数组首地址:" << **mvalues << endl;
// 释放多维数组内存
for(int i = 0; i < col; i++) {
delete[] mvalues[i];
}
delete [] mvalues;
return 0;
}
结果:
一维数组首地址:1
二维数组首地址:2
# 对象的动态内存分配
#include <iostream>
using namespace std;
class Person
{
public:
string name;
Person() {
cout << "Person构造函数!" <<endl;
}
~Person() {
cout << "Person析构函数!" <<endl;
}
};
int main( )
{
Person* person = new Person();
person->name = "Tony"; // 动态创建的对象,不能使用点语法
cout << "name:" << person->name << endl;
delete person; // 释放对象内存
return 0;
}
结果
Person构造函数!
name:Tony
Person析构函数!
⚠️ 如果使用 person.name 会报错:
error: request for member ‘name’ in ‘person’, which is of pointer type ‘Person*’ (maybe you meant to use ‘->’ ?)
# 命名空间
给相同的对象或者函数限定对应的来源,从而在使用时进行区分,避免混淆
# 命名空间
某个文件一个名为 eat() 的函数,在另一个可用的库中也存在一个相同的函数 eat()。
这样,编译器就无法判断您所使用的是哪一个 eat() 函数。
因此,引入了命名空间这个概念,它可作为附加信息来区分不同库中相同名称的函数、类、变量等。
使用了命名空间即定义了上下文。本质上,命名空间就是定义了一个范围。
# namespace 定义
// 命名空间的定义使用关键字 namespace
namespace namespace_name {
// 代码声明
}
示例:
#include <iostream>
using namespace std;
// 第一个命名空间
namespace animal_space{
void eat(){
cout << "吃生食!" << endl;
}
}
// 第二个命名空间
namespace person_space{
void eat(){
cout << "吃熟食!" << endl;
}
}
int main ()
{
animal_space::eat();
person_space::eat();
return 0;
}
结果:
吃生食!
吃熟食!
# using 指定范围
using xxx 指令会告诉编译器,后续的代码将使用指定的命名空间中的名称:
#include <iostream>
using namespace std;
// 第一个命名空间
namespace animal_space{
void eat(){
cout << "吃生食!" << endl;
}
}
// 第二个命名空间
namespace person_space{
void eat(){
cout << "吃熟食!" << endl;
}
}
// 指定后面代码使用的命名空间
using namespace person_space;
// 可以再加上只指定eat(),不加表示整个命名空间的方法,加上限定只在限定的内容
using person_space::eat;
int main ()
{
eat();
eat();
return 0;
}
结果:
吃熟食!
吃熟食!
# 命名空间嵌套
命名空间可以嵌套:
namespace space1 {
// 代码声明
namespace space2 {
// 代码声明
}
}
// 访问 space1 中的成员,也可以使用space2中的(同时可以使用)
using namespace space1;
// 访问 space2 中的成员,不能使用space2外的(使用 :: 运算符来访问嵌套的命名空间中的成员)
using namespace space1::space2;
示例:
#include <iostream>
using namespace std;
// 第一个命名空间
namespace animal_space{
void eat(){
cout << "动物:吃生食!" << endl;
}
// 第二个命名空间
namespace person_space{
void eat(){
cout << "人:吃熟食!" << endl;
}
}
}
// 指定后面代码使用的命名空间
using namespace animal_space::person_space;
int main ()
{
eat();
eat();
return 0;
}
结果:
人:吃熟食!
人:吃熟食!
# 跨文件命名空间
1、同函数一样,可以一个文件声明,一个文件定义
2、命名空间也可以一个文件声明,一个文件定义,甚至多个文件定义补充,不同文件定义不同的元素
文件1:
namespace person_space{
void eat(){
cout << "人:吃熟食!" << endl;
}
}
文件2:
namespace person_space{
void see(){
cout << "人:看!" << endl;
}
}
# 副本机制
以函数参数为例
# 值传参
#include <iostream>
using namespace std;
int showVal(int b)
{
cout << "参数b = " << b << "; 内存地址 = " << &b << endl;
return b;
}
int main()
{
int a = 8;
cout << "变量a = " << a << "; 内存地址 = " << &a << endl;
int c = showVal(a);
cout << "返回c = " << c << "; 内存地址 = " << &c << endl;
}
结果
变量a = 8; 内存地址 = 0x7fffbf417cec
参数b = 8; 内存地址 = 0x7fffbf417ccc
返回c = 8; 内存地址 = 0x7fffbf417ce8
结论:
变量,参数,返回 :
值相同,内存地址不同
也就是创建了三份int类型数据内存
# 引用传值
现在我们将参数和返回都改成引用类型
#include <iostream>
using namespace std;
int* showVal(int* b)
{
cout << "参数b = " << *b << "; 内存地址 = " << b << endl;
return b;
}
int main()
{
int a = 8;
cout << "变量a = " << a << "; 内存地址 = " << &a << endl;
int* c = showVal(&a);
cout << "返回c = " << *c << "; 内存地址 = " << c << endl;
}
结果
变量a = 8; 内存地址 = 0x7ffddfaf1274
参数b = 8; 内存地址 = 0x7ffddfaf1274
返回c = 8; 内存地址 = 0x7ffddfaf1274
结论:
值相同,内存地址相同
说明引用传值前后都是同一个对象。
# 副本构造器
副本构造器就是一个构造方法,复制一份当前对象的克隆对象
一个简单int类型传值和返回
#include <iostream>
using namespace std;
int showVal(int b)
{
return b;
}
int main()
{
int a = 8;
int c = showVal(a);
//其实,上述函数调用等同于下面的流程
int a = 8;
// 参数阶段 showVal(a);
int b;
b = a;
// 返回阶段 return b;
int c;
c = b;
}
下面通过示例解释
示例:
类对象的创建和赋值操作
#include <iostream>
using namespace std;
class Person
{
public:
int age;
// 构造函数
Person(int a) {
age = a;
cout << "执行构造函数, age=" << a << endl;
}
};
void showAge(Person p)
{
cout << "函数值传参数, age=" << p.age << endl;
}
int main()
{
cout << "==== 代码位置 1 ==== " << endl << endl;;
Person person1 = Person(18);
cout << endl;
cout << "==== 代码位置 2 ==== " << endl << endl;;
Person person2 = person1;
cout << endl;
cout << "==== 代码位置 3 ==== " << endl << endl;
person2 = person1;
cout << endl;
cout << "==== 代码位置 4 ==== " << endl << endl;
showAge(person2);
}
结果:
==== 代码位置 1 ====
执行构造函数, age=18
==== 代码位置 2 ====
==== 代码位置 3 ====
==== 代码位置 4 ====
函数值传参数, age=18
我们很难看到赋值过程到底怎么实现的
接着,下面同时实现 自定义 【副本构造函数】 和 【 = 运算符重载】
示例:
#include <iostream>
using namespace std;
class Person
{
public:
int age;
// 构造函数
Person(int a) {
age = a;
cout << "执行构造函数, age=" << a << endl;
}
// 重载 = 运算符
Person operator=(Person p)
{
this->age = p.age;
cout << "执行 ‘=’ 赋值运算, age=" << p.age << endl;
return *this; // 返回自身
}
// 用户定义的副本构造函数
Person(const Person &p)
{
this->age = p.age;
cout << "执行副本构造函数, age=" << p.age << endl;
}
};
void showAge(Person p)
{
cout << "打印获取的函数参数, age=" << p.age << endl;
}
int main()
{
cout << "==== 代码位置 1 ==== " << endl << endl;;
Person person1 = Person(18);
cout << endl << "==== 代码位置 2 ==== " << endl << endl;;
Person person2 = person1; // 注意此时 person2 是未初始化对象
cout << endl << "==== 代码位置 3 ==== " << endl << endl;
person2 = person1; // 注意此时 person2 是已经初始化过的对象
cout << endl << "==== 代码位置 4 ==== " << endl << endl;
showAge(person2);
}
结果
==== 代码位置 1 ====
执行构造函数, age=18
==== 代码位置 2 ====
执行副本构造函数, age=18
==== 代码位置 3 ====
执行副本构造函数, age=18
执行 ‘=’ 赋值运算, age=18
执行副本构造函数, age=18
==== 代码位置 4 ====
执行副本构造函数, age=18
打印获取的函数参数, age=18
小结:
1、编译器会自动提供一个副本构造器
2、如果用户自定义了副本构造器,编译器也不会收回自己提供的副本构造器
3、副本构造器在如下情况会被自动调用:
1.参数的类型是某个类对象,系统将为该输入参数创建一个副本并将其传递到函数里去
2.当函数的返回值的类型是某个类的时候,该函数将创建一个该类对象的副本并把后者返回给自己的调用者。
3.当用户使用某个对象去初始化另一个对象时,会创建一个副本
4、如果指定了副本构造函数,则进行副本复制时,只会对副本构造函数中指定的变量进行初始化。
5、如果未指定副本构造函数,则进行副本复制时,主体中所有已经初始化的变量在副本中也同样都会被初始化,初始化的值于主体相同。
另外:
1、参数和返回添加 const 关键字:可以接受 const 类型和 非const 类型的对象,
2、不加 const 关键字只能接受 非const 类型对象 (如果传入const对象会被错误认为是可以修改的)
问题:
1、对于类对象,如果成员存在指针,那么简单复制后两者拥有相同的指针值,但是指针值是内存地址,也就是变相引用了同一份内存,假如此时函数内修改这个内存数据,就会使得原始对象该成员指针对应值发生改变。
(在OC中 数组的 copy 复制通常是浅复制,也就是容器复制,但是容器内的对象还是地址引用)
2、这种指针复制逻辑使得一个对象成员真实数据被莫名其秒修改了自己确不知道,这显然不是完美的方案。
3、这个时候就需要自定义复制能力,也就是 = 运算符重载和副本构造器中根据自己的需要复制指针地址还是实际内容(OC中因此也存在浅复制和深复制区别)