C++ | For New Learners
本文面向 C++ 的 New Learner. 我将通过五个方面为您介绍 C++ 的基础.
PART1 熟悉您的 IDE
我推荐 Visual Studio 2022, 因为它是一个非常强大的 IDE. 您应当学会一些基本操作, 包括但不限于:
- 编译写好的程序并运行;
- 调试断点分析运行过程;
- 熟悉常用操作的快捷键;
- 管理项目工程目录结构.
在此基础之上, 您也应有属于自己的代码环境与书写风格. 下面是我编写程序所用的界面:
一个工程主体由 ".hpp"(头文件) ".cpp"(源文件) 组成. 一个优秀的 C++ 工程师应当有良好的工程目录管理 (除非您是一名 ACM 选手).
以 VS2022 为例, 在一个简单工程的头文件 demo.hpp 中进行函数定义, 在对应的源文件 demo.cpp 实现函数内容, main.cpp 则是主函数入口, vcxproj 文件中有工程的配置信息, sln 解决方案用于整合它们.
下面给出一个二叉搜索树工程的例子:
tree.hpp
#pragma once
#ifndef _TREE_
#define _TREE_
#include <iostream>
class Node
{
private:
int data;
Node* left;
Node* right;
public:
Node(int tdata = 0, Node* tleft = nullptr, Node* tright = nullptr) :data(tdata), left(tleft), right(tright) {}
~Node() {}
int GetData() { return data; }
void SetData(int tdata) { data = tdata; }
Node* GetLeft() { return left; }
void SetLeft(Node* tleft) { left = tleft; }
Node* GetRight() { return right; }
void SetRight(Node* tright) { right = tright; }
};
class Tree
{
private:
Node* root;
public:
Tree() :root(nullptr) {}
~Tree() { DeleteTree(root); }
Node* GetRoot() { return root; }
Node* Insert(Node* cur, int val);
void CreateTree(int* arr, int n);
void PrintTree(Node* cur);
void DeleteTree(Node* cur);
};
#endif
tree.cpp
#include "tree.hpp"
using namespace std;
Node* Tree::Insert(Node* cur, int val)
{
if (cur == nullptr)
{
cur = new Node;
cur->SetData(val);
}
else if (val < cur->GetData())
{
cur->SetLeft(Insert(cur->GetLeft(), val));
}
else if (val > cur->GetData())
{
cur->SetRight(Insert(cur->GetRight(), val));
}
return cur;
}
void Tree::CreateTree(int* arr, int n)
{
for (int i = 0; i < n; ++i)
{
root = Insert(root, arr[i]);
}
}
void Tree::PrintTree(Node* cur)
{
if (cur == nullptr)
{
return;
}
cout << cur->GetData() << " ";
PrintTree(cur->GetLeft());
PrintTree(cur->GetRight());
}
void Tree::DeleteTree(Node* cur)
{
if (cur == nullptr)
return;
DeleteTree(cur->GetLeft());
DeleteTree(cur->GetRight());
delete cur;
cur = nullptr;
}
main.cpp
#include "tree.hpp"
using namespace std;
int main()
{
int n;
int arr[100] = { 0 };
cin >> n;
for (int i = 0; i < n; ++i)
cin >> arr[i];
Tree tree;
tree.CreateTree(arr, n);
tree.PrintTree(tree.GetRoot());
return 0;
}
注意头文件中加入 #pragma once 或 #ifndef 以防止重复定义. 另外请不要在头文件中使用 using.
当使用 template 模板时, 应在头文件中将其实现. 如果您不喜欢在自定义的头文件中使用 iostream, 您可以定义一个函数指针将其实现交给 main.cpp.
PART2 构造函数与析构函数
构造函数分为默认构造函数, 含参构造函数, 拷贝构造函数.
默认构造函数可以不需要参数, 通过它实例化的对象与普通类型定义的方式很是相似.
class Object
{
private:
int data;
public:
Object(int tdata = 0) :data(tdata) {}
};
int main()
{
Object o;
return 0;
}
含参构造函数需要若干参数, 我们同样可以使用上一个例子:
class Object
{
private:
int data;
public:
Object(int tdata = 0) :data(tdata) {}
};
int main()
{
Object o(1);
return 0;
}
拷贝构造函数适用于对象间的拷贝, 如
Object o1(1);
Object o2 = o1;
如果我们不主动定义拷贝构造函数, 那么系统默认如下浅拷贝的形式:
Object(Object& t)
{
data = t.data;
}
请注意当我们拷贝含指针的对象时, 应采取深拷贝:
class Object
{
private:
int* data;
public:
Object(int val = 0)
{
data = new int;
*data = val;
}
Object(Object& t)
{
data = new int;
*data = *t.data;
}
};
众所周知, 内存分为静态存储区与动态存储区, 静态存储区存放的是静态类型以及全局变量, 动态存储区则包括堆与栈.
静态存储区中的数据会一直保存到程序结束, 栈中的数据在其函数体外会弹出消失. 这不是我们关心的重点, 我们不需要写析构函数来删除它们.
但是堆中的数据是通过 new 得到的, 这些不会被主动释放的数据需要我们亲自去 delete 才能避免内存泄漏.
以前文提到的二叉搜索树为例, 每一个新插入的结点都是保存在堆中, 析构时我们应采取一种策略去释放每一个结点的内存.
void Tree::DeleteTree(Node* cur)
{
if (cur == nullptr)
return;
DeleteTree(cur->GetLeft());
DeleteTree(cur->GetRight());
delete cur;
cur = nullptr;
}
Tree::~Tree() { DeleteTree(root); }
PART3 封装, 继承与多态
这是属于 C++ 的三大特征.
封装意为将程序的内部结构隐蔽起来, 外部调用时只需调用程序的接口, 而不需关注程序的内部信息.
我们给笔记本充电时只需将电源插上, 而不会关注充电器内部的结构.
继承指的是一个子类拥有父类的全部信息, 子类是父类在某一属性上的特化.
老师是一类职业的统称, 因此数学老师应该继承老师这门职业的所有特点.
多态通过父类定义的虚函数, 共以各子类实例化函数将其实现.
数学老师擅长数学分析, 但是生物老师不擅长; 相反地, 生物老师擅长生物实验, 但是数学老师不擅长.
我们来看一个封装的例子:
class Node
{
private:
int data;
Node* left;
Node* right;
public:
Node(int tdata = 0, Node* tleft = nullptr, Node* tright = nullptr) :data(tdata), left(tleft), right(tright) {}
int GetData() { return data; }
void SetData(int tdata) { data = tdata; }
Node* GetLeft() { return left; }
void SetLeft(Node* tleft) { left = tleft; }
Node* GetRight() { return right; }
void SetRight(Node* tright) { right = tright; }
};
data, left, right 都是不能够在类外进行访问的. 如果真的很需要这些参数, 那么就应该用接口 GetData(), GetLeft(), GetRight().
我们来看一个继承的例子:
class Shape
{
protected:
string name;
public:
Shape(string tname = "null") { name = tname; }
void PrintName() { cout << name; }
};
class Rectangle :public Shape
{
private:
double length;
double width;
public:
Rectangle(string tname = "null", double tlength = 0, double twidth = 0) :Shape(tname), length(tlength), width(twidth) {}
};
我们可以在主函数中定义 Rectangle 对象, 并调用 PrintName() 函数, 尽管 PrintName() 函数没有在 Rectangle 类中声明.
我们来看一个多态的例子:
class Shape
{
protected:
string name;
public:
Shape(string tname = "null") { name = tname; }
virtual double GetArea() = 0;
};
class Rectangle :public Shape
{
private:
double length;
double width;
public:
Rectangle(string tname = "null", double tlength = 0, double twidth = 0) :Shape(tname), length(tlength), width(twidth) {}
double GetArea() { return length * width; }
};
class Circle :public Shape
{
private:
double radius;
public:
Circle(string tname = "null", double tradius = 0) :Shape(tname), radius(tradius) {}
double GetArea() { return 3.14 * radius * radius; }
};
我们在 Rectangle 类与 Circle 类中重写了 GetArea() 接口, 使之分别实例化为了可以执行功能的具体函数.
PART4 一些特殊语法
overload
重载分为函数重载与运算符重载.
我们举一些例子:
int max(int x, int y)
{
if (x > y)return x;
return y;
}
double max(double x, double y)
{
if (x > y)return x;
return y;
}
函数重名实现不同功能称为函数重载.
运算符重载语法有些特殊, 下面以重载加法为例
Object operator+(Object& t)
{
Object o;
o.data = data + t.data;
return o;
}
再以重载输出流为例
friend ostream& operator<<(ostream& output, Object& t)
{
output << t.data;
return output;
}
template
模板分为函数模板与类模板.
上述函数重载的例子我们还可以利用函数模板实现:
template<typename T>
T max(T x, T y)
{
if (x > y)return x;
return y;
}
类模板的声明方法与之类似:
template<class T>
class Node
{
private:
T data;
Node<T>* next;
};
enum
枚举类型是由用户定义的若干枚举常量的集合, 如
enum Flag { UNVISITED, VISITED };
Flag 即为一个自定义的类型
Flag flag = VISITED;
typedef
定义类型可用于为类型取一个新的名字
typedef unsigned long long ll;
typedef int array[100];
array arr1, arr2;
它还可以用于函数指针
typedef void (*Function) (int);
Function fun;
fun 是一个以 int 为参数的无返回类型函数.
static
- static 局部变量: 将一个变量声明为函数的局部变量, 那么这个局部变量在函数执行完成之后不会被释放, 而是继续保留在内存中.
- static 全局变量: 表示一个变量在当前文件的全局内可访问.
- static 函数: 表示一个函数只能在当前文件中被访问.
- static 类成员变量: 表示这个成员为全类所共有.
- static 类成员函数: 表示这个函数为全类所共有, 而且只能访问静态成员变量.
const
- const 常量: 定义时就初始化, 以后不能更改.
- const 形参: 该形参在函数里不能改变.
- const 修饰类成员函数: 该函数对成员变量只能进行只读操作.
PART5 熟悉您的 STL
STL 包括容器, 算法和迭代器.
容器 vector 是一个封装的动态数组:
它是顺序存储的, 因此可以元素在序列中的位置访问对应的元素; 它支持直接插入或删除的操作.
int main()
{
int n;
cin >> n;
vector<int>v(n);
for (int i = 0; i < v.size(); ++i)
cin >> v[i];
v.insert(v.begin() + 1, 10);
for (int i = 0; i < v.size(); ++i)
cout << v[i] << " ";
cout << find(v.begin(), v.end(), 3) - v.begin() << endl;
return 0;
}
容器 map 是一个存储键值对的二叉树:
我们常用的数组就是 int arr[10] 是一个 int 到 int 的 map. map 还可以是 int 到 string, char 到 class 等等.
map 在查找键时非常快, 因为它自动对键建立了排序并以平衡二叉树实现, 只需二分查找 (logN) 的时间复杂度.
可以说 map 是 set 的推广, set 中不存储键值.
int main()
{
int n;
cin >> n;
map<char, string>m;
char c;
string s;
for (int i = 0; i < n; ++i)
{
cin >> c >> s;
m.insert(pair<char, string>(c, s));
}
for (auto i = m.begin(); i != m.end(); ++i)
{
cout << i->first << " " << i->second << endl;
}
auto it = m.find('a');
if (it != m.end())
cout << it->first << " " << it->second << endl;
return 0;
}
排序函数 sort(), 查找函数 find() 等都是非常实用的算法工具, 它们都被封装在了 <algorithm> 库中.
到此为止, 您已经具备了 C++ 的编程基础, 可以着手一些简单项目了.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下