【C++高级编程】(一)C++速成
本章内容:
- 简要回顾C++语言最重要的部分及语法
(主要讲述日常编程会遇到的最重要的C++部分,大佬快速浏览即可)
1.1 C++基础知识
- C++是基于C语言的超集,但这两种语言并不一样
1.1.1 小程序的"hello world"
//HelloWorld.cpp #include<iostream> int main() { std::cout << "Hello world" << std::endl; return 0; }
- 注释
- 第一行是注释
- 两种写法:两条斜杠开头(C++风格)或/**/包裹(C风格)
- 注释仅供程序员阅读,编译器自动忽略不运行
- 预处理指令
- 第二行是预处理指令
- 预处理指令以#字符开始
- include 指令首先告诉预处理器,提取<iostream>头文件中所有内容提供给当前文件,<iostream>头文件声明了C++提供的输入输出机制(没有此头文件,将无法执行输入输出文本)
- 在C中,被包含的文件常以 .h 结尾,C++中标准库的头文件则省略了这一后缀
- C中的标准头文件在C++中仍然存在,但换用了新名称(如<stdio.h>变成了<cstdio>)
- main()函数
- main() 函数是程序的入口
- main() 函数返回一个 int 值以指示程序的最终状态
- 输入/输出流
- 可以将输出流想象为数据的滑槽,放入其中的任何内容都可以被正确的输出
- std::cout 就是对应于用户控制台或标准输出的滑槽,还有其他滑槽,包括用于输出错误信息的 std::cerr
- std::cout 输出流可以在一行代码中连续输出多个不同类型的数据(std::cout << "文本" << 123;)
- std::cin 输入流接受用户的键盘输入,但需慎重(永远不知道用户会输入什么奇奇怪怪的东西)
- std::endl 代表序列的结尾换行,另一种换行符是:转义字符\n
- \n 换行
- \r 回车
- \t 制表符
- \\ 一个反斜杠
- \" 一个引号
1.1.2 命名空间
using namespace std;
- :: 被称为作用域解析运算符
- 命名空间用来处理不同代码段间的名称冲突问题(如:第三方库中有Func()函数,而自己也写了Func()函数,就可以把自己写的放进 namespace mycode{ Func(){} },从而调用 mycode::Func(); )
- 使用 using 指令避免预先指明命名空间,使用 using namespace mycode,就可以直接调用 Func();
#include<iostream> #include"namespace.h" namespace mycode { int A = 0; void Func() { std::cout << "Func()" << std::endl; } } //将Func()函数放进命名空间之后,就与第三方库函数Func()区分开来,可调用: mycode::Func();
mycode::A;
上面代码等价于:
#include<iostream> #include"namespace.h" using namespace mycode { int A = 0; void Func() { std::cout << "Func()" << std::endl; } } //将Func()函数放进命名空间之后,就与第三方库函数Func()区分开来,可调用: Func();
A;
一个源文件中可以包含多个 using 指令,此方法虽然快捷,但不要过度使用
- 使用整个命名空间:using namespace [name];
- 使用命名空间中的变量:using [name]::[variable];
- 使用默认命名空间中的变量:::[variable];
1.1.3 变量
- 声明变量时,可以不指定值(未初始化的变量通常被赋予一个半随机值,bug)
- C++中常见变量类型:
int a; short b; long c; long long d; // unsigned 对上面类型加以限制,使之为正数 unsigned int aa; unsigned short bb; unsigned long cc; unsigned long long dd; //浮点型(单/双精度) float e = 3.14f; double f; long double g; //字符型(单个字符,单个16位字符,单个32位字符,单个宽字符) char h = 'm'; char16_t h1 = u'm'; char16_t h2 = U'm'; wchar_t h3 = L'm'; //布尔型,结果返回 true / false bool i = true; //auto 编译器自动判断类型 auto j = 10; //应该为int型
- C++没有提供基本的字符串类型,但作为标准库的一部分提供了字符串的标准实现
3. 三种显式转换变量类型:
bool IfBool = (bool) IfInt; bool IfBool = bool(IfInt); bool IfBool = static_cast<bool>(IfInt);
- 在某些环境中,可以自动执行类型转换 / 强制转换
- 但有时,自动类型转换存在潜在的数据丢失(如:float 转换为 int 会丢失小数部分)
1.1.4 运算符
- 如果记不住C++运算符的优先级,建议使用括号将表达式分组
1.1.5 类型
- 枚举类型
enum Color{red = 2,green,blue = 5,pink}; //默认情况下,所枚举元素第一个名称的值为0,第二个名称的值为1,第三个名称的值为2,以此类推。(枚举值不能同名) //但此处设置了初始化值,默认情况下,每个名称都会比它前面一个名称大1,所以:red = 2,green = 3,blue = 5,pink = 6 int main() { Color a = Color::red; switch (a) { case red: cout << a << endl; //red = 2 break; case green: cout << a << endl; //green = 3 break; case blue: cout << a << endl; //blue = 5 break; default: cout << a << endl; //pink = 6 break; } return 0; }
2. 强枚举类型
上面给出的枚举并不是强类型的,这意味着其并非类型安全的,基本上会被当作整型数据 int 解释
enum class Color{red = 2,green,blue = 5,pink}; //Color 是一个类型安全的枚举,其枚举值不会自动转换为整数,枚举名称不会自动超出封闭的作用域
- 默认情况下,枚举值的基本类型是整型
3. 结构
结构允许将一个或多个已有类型封装到一个新的类型中
Struct Student{ string Name; //学生姓名 int age; //学生年龄 int score; //学生分数 }; //分号
1.1.6 条件
1. if / else 语句
if(){} else if(){} else{} //()圆括号内的结果必须是布尔值
2. switch 语句
switch(){ case 1: //执行代码 break; case 2: //执行代码 break; .... default: //执行代码 break; } //如果省略 break ,将会无视case判断,直接执行后面的代码
3. 三元运算符
(i = 0) ? 1 : 2; // 判断括号内是否正确,正确返回1,错误返回2 //相当于 if(i = 0){ return 1; } else{ return 2; }
1.1.7 循环
1. while 循环
while(){ //执行代码; }
2. do / while 循环
do { //执行代码; } while();
3. for 循环
for(int i=0; i; i++){ //执行代码; }
4. 基于区间的for循环
int arr[] = {1,2,3,4}; for(auto &i : arr){ i+=2; }
1.1.8 数组
- 声明数组时,必须用常量值来声明数组的大小
- 数组第一个元素的位置是0
int Array[10] = {0}; //声明一个长度为10且所有值初始化为0的数组
3. std::array
-
- 在C++11之前, 如果想用更安全的数组类型,应该使用 std::vector(可自动扩容,当空间已满时会自动扩到原来2倍,有时造成空间浪费)
- C++11引入了 std::array 的新型容器,该容器在 <array> 头文件中定义
- 优点:
- 知道自身大小(有固定大小,运行时数组不会增大缩小);
- 不会自动转换为指针;
- 具有迭代器,可以方便地遍历元素
- (由于支持迭代器,STL的算法在 std::array 中也有效)
#include<iostream> #include<array> //头文件 using namespace std; int main() { array<int, 3>arr = { 4,5,6 }; cout << "数组大小:" << arr.size() << endl; //数组大小:3 for (auto i : arr) //遍历数组 { cout << i << endl; } return 0; }
1.1.9 函数
- 如果函数在某个特定文件内部使用,通常会在源文件中声明并定义这个函数;
- 如果函数是供其他模块或文件使用的,通常会在头文件中声明函数,并在源文件中定义函数
//函数的声明: void Func(int a) { cout << a << endl; } //void 指明该函数无返回值return
3. C++11中,每个函数都有一个预定义的局部变量 __func__ (const char的一个静态数组),用于存放函数的名字
static const char __func__[] = "function-name";
- 还可以将这个变量用于日志
1.2 深入研究C++
1.2.1 指针及动态内存
(动态内存允许所创建的程序具有在编译时大小可变的数据,大部分复杂的程序都会使用动态内存)
- 栈和堆
-
- C++程序中的内存被分为两个部分:堆、栈
2. 动态分配的数组
-
- 由于栈的运行方式,编译器在编译时必须能够判断每个栈的大小,因此栈的大小被预先确定,从而不能声明大小可变的数组(数组长度不允许使用变量:arr[n] )
- 使用动态内存将数组放入堆,这样就可以在运行时指定数组的大小
int ArraySize = 10; int Array[ArraySize]; //数组放在栈中,编译器需要知道数组的确切大小,因此不允许使用变量 //1.为了动态分配数组,首先要声明一个指来指去的指针 int *p; //int 类型后的*说明变量指向堆中某段整型内存(可将指针想象成一个箭头,它指向动态分配的堆内存,这个指针尚未指向任何具体内容,还是一个未赋值未初始化的变量) //2.为了用新的堆内存初始化指针,使用new p = new int [ArraySize]; //内存大小与ArraySize变量对应,指针变量仍在栈中,但动态创建的数组在堆中 //3.现在已经分配好了内存,可将Array当作普通数组使用 Array[3] = 10; //4.假如用完了数组,一定要用delete释放空间 delete []Array; //delete后的方括号表示删除一个数组
-
- 调用new就要相应的调用delete释放,以免造成内存泄漏(同理,malloc对应free)
- 为避免常见的内存问题,应该使用智能指针。智能指针对象超出作用域时,会自动释放内存
#include<memory> //头文件 unique_ptr<int[]>Array(new int[ArraySize]); //unique_ptr版本可以自动释放内存
3. 使用指针
-
- 除了动态分配内存外,还可以将任意变量放到堆中
int *a = new int; //此时,指针指向一个整型值的地址,为了访问这个值,需要对指针解除引用 //在解除引用前,指针必须有效(对null或未初始化的指针解引用会导致程序崩溃) int num = 10; int *a = # //为了让指针指向某个变量,需要使用&取址运算符
-
- 指向结构的指针
- 通常向函数传递变量是按值传递;
- 使用指向堆栈变量的指针以允许函数修改其堆栈中的变量,是按引用传递
1.2.2 C++中的字符串
1. C风格字符串
-
- 将字符串看成字符数组
char ArrayString[20] = "Hello World"; //编译器在栈中分配了20个字符的空间,以'H'开头,以'\0'结尾('\0'表示字符串的结尾),'\0'告诉代码字符串结尾的位置 const char *p = "Hello World"; //编译器在栈中分配足够的内存以存放指针p,指针指向编译器存放该常量字符串"Hello World"分配的内存区域,'d'后面也有一个'\0'字符结尾
- 将字符串看成字符数组
2. C++风格字符串
-
- 将字符串封装在一个易于使用的string类型中
#include<string> //<string>头文件中string类型位于std命名空间中 string s = "Hello World"; //对于C风格的字符串使用==时,实际上是比较字符数组的地址,而非内容;
//对于C++的string,==是比较两个string
- 将字符串封装在一个易于使用的string类型中
3. 非标准字符串
-
- 开发架构及操作系统倾向于使用自己的字符串表示方式
- 开始一个C++项目时,提前决定如何表示字符串非常重要
1.2.3 引用
- 传值,不影响变量
void Func1(int i) { i++; }
- 传引用,修改了原始变量的值
void Func(int &i) { i++; }
1.2.4 异常
- C++是一种非常灵活的语言,但并不十分安全。异常就是试图增加一点安全性的语言特性
- 异常是一种无法预料的情形,伴随着一些新术语,当某段代码检测到异常时,就会抛出一个异常
-
- throw:当问题出现,程序抛出一个异常。抛异常使用throw关键字完成。
- catch:用于捕捉异常。catch(...)可以捕获任意类型的异常,主要时用来捕获没有显示捕获类型的异常。相当于条件判断中的else。
- try:try中包含会出现异常的代码或者函数。后面通常会跟一个或者多个catch块。
1.2.5 const 的多种用法
- const 定义常量
const int a = 10; //在C语言中,程序员经常使用预处理器#define机制进行声明; //而C++鼓励使用 const 取代 #define 定义常量
- 使用 const 保护参数
void Func(const char* s) {} //调用函数Func()时,char* 会自动转换为 const char* ,提供一定程度的保护,防止其他代码修改变量
- const 引用
const 引用其主要价值在于效率:函数传值时需要一个完整的副本;而传引用时实际只是传递了一个指向原始数据的指针(这样计算机就不需要制作副本);通过传递 const 引用,可以两者兼顾:不需要副本,原始变量也不会被修改
1.3 作为面向对象语言的C++
- 声明类
-
- 类定义了对象的特征
- 在C++中,类通常在头文件中声明,在对应源文件中完整定义
//在.h 头文件 class ATest //首先定义一个类名称ATest { public: //公有访问域:声明类的方法(行为) int getA() { return A; }; void setA(int num) {}; protected: //私有访问域:声明类的数据成员(属性) int A; }; //分号
1.4 标准库
- 使用标准库中的类:
-
- 使用标准库中的类性能更高(经过时间检验),比使用自己的类效率更高
- 不需要重新创建某些类,也不需要浪费时间重新造轮子
- 上面涉及的一些标准库中的类:如 std::string、std::cout 类等
- 标准库提供的容器概念:如 std::vector
- 以一种更灵活安全的机制取代了C中数组的概念
- 不用担心内存的管理,vector 会自动分配足够的内存来存放元素
- vector 是动态的,可以在运行时添加 / 删除元素
- 为了更方便的遍历容器中的内容,标准库还提供了迭代器的概念
#include<iostream> #include<vector> //头文件 #include<string> using namespace std; int main() { //A 被声明为vector<string>,尖括号内用来指定模板参数 //vector 是一个泛型容器,几乎可以容纳任何类型的对象 vector<string> A = {"Hello World","Hello My World"}; //为了向 vector 中添加元素,调用 push_back() 方法 A.push_back("Hi"); //使用 auto 关键字让编译器自动判断变量B的类型 for (auto B = A.cbegin(); B != A.cend(); ++B) { cout << *B << endl; } for(auto & str:A) { cout << str << endl; } return 0; }
1.5 一个C++程序案例
- 此案例属上文知识的汇总,使用了上面讲到的类、异常、流、向量、迭代器、命名空间、引用及其他语言特性
- 建立一个雇员数据库,包含以下功能:
- 添加雇员
- 删除雇员
- 雇员晋升
- 查看所有雇员(以前和现在的雇员)
- 查看当前雇员
- 查看以前雇员
- 程序分为三大部分:
- Employee 类(封装单个雇员信息)
- DataBase 类(管理公司所有雇员)
- 单独的一份 UserInterface 文件提供程序接口
1.5.1 Employee 类
Employee 类用于维护某个雇员的全部信息
- Employee.h
// Employee.h文件:声明了Employee类的行为 #include<string> namespace Records { //新雇员的默认起薪,存于Records命名空间中; //Records中其他代码可以通过常量kDefaultStartingSalary访问, //而其他位置需通过Records::kDefaultStartingSalary来引用 const int kDefaultStartingSalary = 30000; //声明Employee类 class Employee { public: //公有方法 Employee(); //构造函数 void promote(int inRaiseAmount = 1000); void demote(int inDemeritAmount = 1000); void hire(); void fire(); void displayer()const; void setFirstName(std::string inFirstName); std::string getFirstName()const; void setLastName(std::string inLastName); std::string getLastName()const; void setEmployeeNumer(int inEmployeeNumber); int getEmployeeNumer()const; void setSalary(int inNewSalary); int getSalary()const; bool getInHired()const; protected: //私有数据成员 std::string mFirstName; std::string mLastName; int mEmployeeNumber; int mSalary; bool bHired; }; }
- Employee.cpp
// Employee.cpp文件:Employee类方法的实现 #include<iostream> #include"Employee.h" using namespace std; namespace Records { //构造函数初始化数据成员:新雇员无名,雇员号-1,薪水为默认值,状态为未雇佣 void Employee::Employee() { mFirstName=""; mLastName=""; mEmployeeNumber = -1; mSalary = kDefaultStartingSalary; bHired = false; } //promote()和demote()方法只是简单调用setSalary()方法 void Employee::promote(int inRaiseAmount) { setSalary(getSalary() + inRaiseAmount); } void Employee::demote(int inDemeritAmount) { setSalary(getSalary() - inDemeritAmount); } //hire()和fire()方法设置了bHired数据成员 void Employee::hire() { bHired = true; } void Employee::fire() { bHired = false; } //displayer()方法使用控制台输出流显示当前雇员信息 //可以直接访问内部数据成员,而不需要使用get存取器 void Employee::displayer() const { cout << "Employee:" << getFirstName() << ", " << getLastName() << endl; cout << "------------------------------------------------------" << endl; cout << (bHired ? "Current Employee" : "Former Employee") << endl; cout << "Employee Number: " << getEmployeeNumer() << endl; cout << "Salary: $" << getSalary() << endl; cout << endl; } //使用存取器get和设置器set,而不建议将数据成员公开 void Employee::setFirstName(string inFirstName) { mFirstName = inFirstName; } string Employee::getFirstName() const { return mFirstName; } }
- EmployeeTest.cpp
// EmployeeTest.cpp文件:Employee类方法的实现 #include<iostream> #include"Employee.h" using namespace std; using namespace Records; int main() { Employee emp; emp.setFirstName("Ann"); emp.setLastName("Tom"); emp.setEmployeeNumer(888); emp.setSalary(100000); emp.promote(); emp.promote(50); emp.hire(); emp.displayer(); return 0; }
1.5.2 DataBase 类
DataBase 类使用了标准库中的 std::vector 类存储 Employee 对象
- DataBase.h
// DataBase.h文件:DataBase类方法的实现 #include<iostream> #include<vector> #include"Employee.h" namespace Records { //雇员编号初始化 const int kFirstEmployeeNumber = 1000; class DataBase { public: DataBase(); ~DataBase(); //通过姓名添加雇员 Employee& addEmployee(std::string inFirstName, std::string inLastName); //get的两个版本:可以通过雇员号检索,或雇员姓名检索 Employee& getEmployee(int inEmployeeNumber); Employee& getEmployee(std::string inFirstName, std::string inLastName); //输出所有雇员,在职雇员,离职雇员 void displayAll() const; void displayCurrent() const; void displayFormer() const; protected: //mEmployees包含了Employee对象 std::vector<Employee> mEmployees; //数据成员mNextEmployeeNumber跟踪新雇员的雇员号 int mNextEmployeeNumber; }; }
- DataBase.cpp
// DataBase.cpp文件:数据库管理 #include<iostream> #include<stdexcept> #include"DataBase.h" using namespace std; namespace Records { //构造函数将下一雇员号初始化 DataBase::DataBase() { mNextEmployeeNumber = kFirstEmployeeNumber; } DataBase::~DataBase() { } //添加雇员 Employee& DataBase::addEmployee(std::string inFirstName, std::string inLastName) { //首先创建一个新Employee对象,并填充数据 //mNextEmployeeNumber的值递增,下一雇员将获得新的编号 Employee theEmployee; theEmployee.setFirstName(inFirstName); theEmployee.setLastName(inLastName); theEmployee.setEmployeeNumer(mNextEmployeeNumber++); theEmployee.hire(); mEmployees.push_back(theEmployee); // TODO: 在此处插入 return 语句 return mEmployees[mEmployees.size() - 1]; } //两个getEmployee()的运行方式相似 Employee& DataBase::getEmployee(int inEmployeeNumber) { //使用迭代器遍历mEmployees中的所有雇员 //(如果编译器不支持auto,可使用vector<Employee>::iterator 替换) for (auto iter = mEmployees.begin(); iter != mEmployees.end(); ++iter) { //检查是否有与传递该方法的信息匹配的员工 if (iter->getEmployeeNumer() == inEmployeeNumber) { return *iter; } } //没有则输出错误信息,并抛出异常 cerr << "No Employee with number" << inEmployeeNumber << endl; throw exception(); } //显示的方法同理,遍历所有雇员,如果符合显示标准就通知雇员显示自己 void DataBase::displayAll() const { //(如果编译器不支持auto,可使用vector<Employee>::const_iterator 替换,因为是const成员函数) for (auto iter = mEmployees.begin(); iter != mEmployees.end(); ++iter) { iter->displayer(); } } void DataBase::displayCurrent() const { for (auto iter = mEmployees.begin(); iter != mEmployees.end(); ++iter) { if (iter->getInHired()) { iter->displayer(); } } } }
- DataBaseTest.cpp
// DataBaseTest.cpp文件:数据库基本功能的简单测试实现 #include<iostream> #include"DataBase.h" using namespace std; using namespace Records; int main() { DataBase DB; Employee& emp1 = DB.addEmployee("Greg", "Wallis"); emp1.fire(); Employee& emp2 = DB.addEmployee("Scott", "Klep"); emp2.setSalary(20000); Employee& emp3 = DB.addEmployee("Nick", "Solter"); emp3.setSalary(120000); emp3.promote(); cout << "All Employee:" << endl << endl; DB.displayAll(); cout << "Current Employee:" << endl << endl; DB.displayCurrent(); cout << "Former Employee:" << endl << endl; DB.displayFormer(); return 0; }
1.5.3 用户界面
- UserInterface.cpp
// UserInterface.cpp文件:基于菜单的用户界面,方便用户使用雇员数据库 #include<iostream> #include<stdexcept> #include"DataBase.h" using namespace std; using namespace Records; int displayMenu(); void doHire(DataBase& inDB); void doFire(DataBase& inDB); void doPromote(DataBase& inDB); void doDemote(DataBase& inDB); //输出菜单,并获取用户输入(在此假定用户能正常输入) int displayMenu() { int selection; cout << endl << "Employee DataBase" << endl << "--------------------------------------" << endl; cout << "1)Hire a new employee" << endl << "2)Fire an employee" << endl << "3)Promote an employee" << endl << "4)List all employees" << endl << "5)List all current employees" << endl << "6)List all previous employees" << endl << "0)Quit" << endl << endl << "--------------------------------------" << endl; cin >> selection; return selection; } //获取用户输入的新雇员名字,并通知数据库添加雇员 void doHire(DataBase& inDB) { string firstName; string lastName; cout << "FirstName?" << endl; cin >> firstName; cout << "LastName?" << endl; cin >> lastName; //遇到错误时,会输出一条信息并继续 try{ inDB.addEmployee(firstName, lastName); } catch (const std::exception&) { cerr << "Unable to add new employee!" << endl; } } //doFire()和doPromote()都会根据雇员号找到雇员数据,然后使用Employee的方法进行修改 void doFire(DataBase& inDB) { int employeeNumber; cout << "Employee Number?" << endl; cin >> employeeNumber; try { Employee& emp = inDB.getEmployee(employeeNumber); emp.fire(); cout << "Employee:" << employeeNumber << "terminated." << endl; } catch(const std::exception&){ cerr << "Unable to terminate employee!" << endl; } } void doPromote(DataBase& inDB) { int employeeNumber; int raiseAmount; cout << "Employee Number?" << endl; cin >> employeeNumber; cout << "How much of a raise?" << endl; cin >> raiseAmount; try { Employee& emp = inDB.getEmployee(employeeNumber); emp.promote(raiseAmount); } catch (const std::exception) { cerr << "Unable to promote employee!" << endl; } } //main函数是一个显示菜单的循环 int main() { DataBase employeeDB; bool done = false; while (!done) { int selection = displayMenu(); switch (selection) { case 1: doHire(employeeDB); break; case 2: doFire(employeeDB); break; case 3: doPromote(employeeDB); break; case 4: employeeDB.displayAll(); break; case 5: employeeDB.displayCurrent(); break; case 6: employeeDB.displayFormer(); break; case 0: done = true; break; default: cout << "Unkonwn." << endl; } } return 0; }
如果不理解上面程序,建议回顾前面的内容;如果仍不甚明了,最好的学习方法是多写多敲
-END-
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix