C++ | For New Learners

本文面向 C++ 的 New Learner. 我将通过五个方面为您介绍 C++ 的基础.

PART1 熟悉您的 IDE

我推荐 Visual Studio 2022, 因为它是一个非常强大的 IDE. 您应当学会一些基本操作, 包括但不限于:

  1. 编译写好的程序并运行;
  2. 调试断点分析运行过程;
  3. 熟悉常用操作的快捷键;
  4. 管理项目工程目录结构.

在此基础之上, 您也应有属于自己的代码环境与书写风格. 下面是我编写程序所用的界面:

一个工程主体由 ".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

  1. static 局部变量: 将一个变量声明为函数的局部变量, 那么这个局部变量在函数执行完成之后不会被释放, 而是继续保留在内存中.
  2. static 全局变量: 表示一个变量在当前文件的全局内可访问.
  3. static 函数: 表示一个函数只能在当前文件中被访问.
  4. static 类成员变量: 表示这个成员为全类所共有.
  5. static 类成员函数: 表示这个函数为全类所共有, 而且只能访问静态成员变量.

const

  1. const 常量: 定义时就初始化, 以后不能更改.
  2. const 形参: 该形参在函数里不能改变.
  3. 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++ 的编程基础, 可以着手一些简单项目了.

posted @   rzk_零月  阅读(66)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下
点击右上角即可分享
微信分享提示