从Java到C再到C++的学习笔记
前言
前面我们简单的对比与学习入门了 C语言,C语言以其简单、高效和直接操作内存的能力,广泛应用于操作系统、嵌入式系统及高性能计算等领域。
学习C语言不仅能够让程序员更好地理解计算机的运行机制,也为后续的C++学习打下坚实的基础。
而 C++ 则可以看作是 C语言的"升级版"。它在 C 的基础上引入了面向对象的编程思想,使得代码的组织和复用更加高效。C++ 的特性,比如类、模板和标准模板库,让编写复杂程序变得更加轻松。
在学习 C语言的时候我们发现其实它和 Java 很不一样,不是面向对象的,只是有一些所有程序语言基本通用的一些关键字和语法类似,在底层的指针与内存等其他特性上完全不同。
而 C++ 在C语言的基础上又有C语言的特殊特性又有面向对象的特性,也更适应我们 Java 从业者的学习入门。
从 Java 到 C语言,就像学自行车时拆掉辅助轮。在这里没有new
关键字帮你打理内存,得自己用malloc
和free
当包工头。指针就像一把没刀鞘的匕首,稍不留神就会划伤自己(段错误警告!),但正是这种"危险"让你真正理解内存如何运作。
从 C语言到 C++ 呢,就像给自行车装上涡轮增压,直接起飞。当你受够了C语言的结构体函数,C++的类就像突然开挂:用class
把数据和操作打包成乐高积木,vector
比Java的ArrayList更快更直接,智能指针让你既能享受自动内存管理,又不失去对底层的控制权。
在本文中我们将一起探索从 Java 到 C,再到C++的过程,看看 C++ 对比 C语言的扩展,以及面向对象与类的一些新特性。
一、 C++的新特性 (此篇幅简单介绍对比C的改动,为什么要怎么改)
C++作为C语言的扩展,引入了许多强大的特性,使得程序开发变得更加高效和灵活。以下是C++相较于C语言的主要扩展和新功能的介绍:
1.1 类和对象,面向对象
C++引入了面向对象编程(OOP)的概念,允许程序员通过"类"来定义数据类型,并可以将数据和操作封装在一起。这样,数据结构和相关操作可以更好地组织和管理。C语言使用结构体(struct)来组织数据,但没有内置的封装和继承机制。
C语言的局限,数据与行为分离
arduino
struct Point {
int x;
int y;
};
void printPoint(struct Point p) {
printf("(%d, %d)", p.x, p.y);
}
C++的解决方案:
arduino
class Point {
private: // 访问控制
int x;
int y;
public:
// 构造函数(自动初始化)
Point(int x, int y) : x(x), y(y) {}
// 成员函数(行为封装)
void print() const { // const保证不修改对象
std::cout << "(" << x << ", " << y << ")";
}
// 操作符重载
Point operator+(const Point& other) const {
return Point(x + other.x, y + other.y);
}
};
是不是很有 Java 对象的味道了。
1.2 模板
C++的模板允许程序员编写与类型无关的代码,使得函数和类可以操作任意数据类型。这种机制大幅提高了代码的复用性和灵活性,在编写泛型程序时尤为重要。
C语言的局限,需要为不同类型编写相同逻辑
css
int max_int(int a, int b) { return a > b ? a : b; }
float max_float(float a, float b) { return a > b ? a : b; }
C++的模板魔法:
c
template <typename T>
T max(T a, T b) {
return a > b ? a : b;
}
// 自动生成int/float等版本
std::cout << max(10, 20); // 调用max<int>
std::cout << max(3.14, 2.99); // 调用max<double>
是不是很有 Java 泛型的味道了。
1.3 标准模板库
C++提供了标准模板库(STL),其中包含了许多通用的数据结构和算法。STL中的容器(如vector、list、map等)和算法(如排序、查找等)使得编写复杂数据处理代码变得更加简便。
C语言的局限,需要自己管理动态数组
c
// 手动管理动态数组
int* arr = malloc(10 * sizeof(int));
arr = 100;
// 需要手动记录容量、元素个数...
C++的现代化容器:
c
#include <vector>
#include <unordered_map>
std::vector<int> nums = {1, 2, 3}; // 动态数组(对比Java ArrayList)
nums.push_back(4); // 自动扩容
std::unordered_map<std::string, int> scores; // 哈希表(对比Java HashMap)
scores["Alice"] = 90;
TL三大件:
- 容器:vector/list/map/set...
- 算法:sort/find/transform...
- 迭代器:统一访问接口(类似Java的Iterator)
1.4 异常处理
C++引入了异常处理机制,通过try、catch和throw关键字来捕捉和处理运行时错误。这种机制使得程序在遇到错误时能够优雅地处理,而不至于直接崩溃。
C语言的错误处理:
ini
FILE* file = fopen("data.txt", "r");
if (file == NULL) {
perror("打开文件失败"); // 依赖返回值检查
return -1;
}
C++的异常机制:
c
try {
std::ifstream file("data.txt");
if (!file) throw std::runtime_error("文件打开失败");
// 读取文件内容...
}
catch (const std::exception& e) {
std::cerr << "错误:" << e.what();
}
是不是很有 Java 的味道,语法类似 try-catch,但 C++ 没有 finally 的。
1.5 命名空间
C++引入了命名空间来解决名称冲突的问题。通过使用命名空间,程序员可以将相关的函数和类组织在一起,从而避免与其他代码库中的名称冲突。
C 语言的命名冲突:
csharp
// math.h
void log() { /* 数学日志 */ }
// user.c
void log() { /* 用户日志 */ } // 重定义错误!
C++的命名空间:
arduino
namespace Math {
void log() { /* ... */ }
}
namespace User {
void log() { /* ... */ }
}
Math::log(); // 明确作用域
using User::log; // 引入当前作用域
有点类似 Java 的包机制。
1.6 智能指针
在C++中,智能指针(如std::unique_ptr和std::shared_ptr)提供了一种自动管理内存的方法,减少了内存泄漏和悬挂指针的风险。智能指针可以像普通指针一样使用,但它们在超出作用域时会自动释放资源。
C 语言的手动内存管理:
c
int* arr = malloc(10 * sizeof(int));
// ...使用后必须
free(arr);
C++的智能指针:
c
#include <memory>
// 独占所有权(类似Java的强引用)
std::unique_ptr<int[]> arr = std::make_unique<int[]>(10);
// 共享所有权(引用计数)
std::shared_ptr<Object> obj = std::make_shared<Object>();
内存安全三剑客:
- unique_ptr:禁止复制的独占指针
- shared_ptr:引用计数的共享指针
- weak_ptr:打破循环引用的观察指针
1.7 增强for循环
C++提供了增强的for循环(范围for循环),使得遍历容器变得更简单。通过这种语法,程序员可以避免使用迭代器或索引来访问元素。
C 语言传统for循环
ini
for (int i = 0; i < vec.size(); ++i) {
std::cout << vec[i];
}
C++ 的增强for循环:
c
for (auto num : vec) { // 自动类型推导
std::cout << num;
}
这里也是很有 Java 的味道。
1.8 指针与引用的区别
C++中的引用是另一个重要特性。与C语言中的指针相比,引用提供了一种更安全且更直观的方式来处理变量。引用不能为nullptr,而且在使用时更加简洁。
C的指针问题:
ini
void swap(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
C++的引用方案:
ini
void swap(int& a, int& b) { // 更直观的语法
int temp = a;
a = b;
b = temp;
}
int x = 10, y = 20;
swap(x, y); // 无需取地址
引用的底层仍是指针实现,只是语法更安全(不能为NULL,必须初始化)。
这一点是基于 C 语言的扩展,这个比较重点下面会详细说。
1.9 其他关键字如 static const extren volatile 等
C++引入了一些新的关键字,增强了语言的功能性。例如:
- static:在类中定义静态成员,允许共享数据。
- const:用于定义常量,确保数据不可修改。
- extern:用于声明全局变量,支持跨文件的链接。
- volatile:用于指示变量可能会被外部因素修改,避免编译器优化。
1.10 类的改动,如类的访问权限控制,类的构造,析构,赋值,拷贝函数等
C++对类的定义和使用进行了大幅扩展,增加了许多特性:
- 访问控制(public、private、protected):提供了更好的封装。
- 构造函数和析构函数:在对象创建和销毁时自动调用。
- 赋值运算符和拷贝构造函数:处理对象之间的赋值和复制。
scss
class Student {
public:
// 构造函数
Student(std::string name) : name(name) {}
// 拷贝构造函数
Student(const Student& other) { /*...*/ }
// 移动构造函数(C++11)
Student(Student&& other) noexcept { /*...*/ }
// 析构函数
~Student() { /* 释放资源 */ }
};
这个也是新增的内容,类似 Java 的类,但是有很多内容也不同,下面会重点介绍。
1.11 现代C++新特性(C++11)
类型推导(C++11):
ini
auto num = 10; // int
auto name = "Alice"; // const char*
Lambda表达式(C++11):
c
std::vector<int> nums {5, 3, 8};
std::sort(nums.begin(), nums.end(), [](int a, int b) {
return a > b; // 降序排序
});
C vs C++关键特性
C++ 通过引入面向对象编程、模板、异常处理等新特性,极大地扩展了C语言的功能。学习和掌握这些特性,能够帮助程序员写出更高效、更易维护的代码,也为后续复杂项目的开发打下基础。
在接下来的章节中,我们将继续深入探索C++的其他特性和应用。
二、C++的数据类型
C++的数据类型主要可以分为基本数据类型、用户定义的数据类型(如类和结构体)以及标准库提供的数据类型。例如,字符串和数组等。了解这些数据类型是编写有效C++程序的基础。
2.1 基本数据类型
C++ 与 C 语言共享了许多基本数据类型,但也增加了一些新的类型。基本数据类型包括:
- 整型:int、short、long、long long,以及对应的无符号类型(如unsigned int)。
- 字符型:char,用于表示字符,通常占用1个字节。
- 浮点型:float、double、long double,用于表示实数。
- 布尔型:bool,用于表示真(true)和假(false)。
ini
int a = 5; // 整型
float b = 3.14f; // 单精度浮点型
char c = 'A'; // 字符型
bool d = true; // 布尔型
2.2 字符串 string 的操作
我知道你们很急,它来了。C语言的字符串太难操作了,还是 Java 的那种字符串更合我们的心意,这不 C++ 的字符串对象来了。
C++ 标准库提供了 std::string 类,简化了字符串操作。与C语言中的字符数组相比,std::string 具有更强的灵活性和安全性,支持动态大小调整和许多方便的操作。
创建字符串:
c
#include <string>
std::string str1 = "Hello"; // 使用字符串字面量
std::string str2("World"); // 使用构造函数
字符串连接:
c
std::string str3 = str1 + " " + str2; // 连接字符串
字符串长度:
ini
size_t length = str3.length(); // 获取字符串长度
查找和替换:
c
size_t pos = str3.find("World"); // 查找子字符串
if (pos != std::string::npos) {
str3.replace(pos, 5, "C++"); // 替换子字符串
}
获取字符:
ini
char firstChar = str3[0]; // 获取第一个字符
遍历字符串:
c
for (char ch : str3) {
std::cout << ch; // 输出每个字符
}
截取字符串:
c
std::string s2 = "Hello"; // 直接赋值
std::string sub = s2.substr(2); // 截取子串
字符串流处理:数字转字符串,字符串转数字
c
#include <sstream>
// 数字转字符串
std::ostringstream oss;
oss << 3.14 << " is pi";
std::string str = oss.str(); // "3.14 is pi"
// 字符串解析数字
std::istringstream iss("42 3.14");
int num; double pi;
iss >> num >> pi; // num=42, pi=3.14
2.3 数组操作
C++数组是同一类型元素的集合,可以通过索引访问每个元素。与C语言一样,C++中的数组大小在声明时需要指定,但在使用std::array或std::vector时,可以动态管理数组的大小。
C 语言风格数组:
c
int arr[5] = {1, 2, 3, 4, 5}; // 静态数组
for (int i = 0; i < 5; ++i) {
std::cout << arr[i] << " "; // 遍历数组
}
std::cout << std::endl;
C++数组(std::array):
c
#include <array>
std::array<int, 5> arr = {1, 2, 3, 4, 5}; // C++数组
for (const auto& elem : arr) {
std::cout << elem << " "; // 遍历
}
std::cout << std::endl;
std::cout << "数组大小: " << arr.size() << std::endl; // 获取大小
C++ 动态数组(std::vector)
std::vector是C++标准库提供的动态数组,支持动态调整大小,适合需要频繁修改数组大小的情况。使用std::vector可以轻松地添加、删除元素,并且可以通过迭代器进行遍历。
c
#include <vector>
std::vector<int> vec; // 动态数组
vec.push_back(10); // 添加元素
vec.push_back(20);
vec.push_back(30);
for (const auto& num : vec) {
std::cout << num << " "; // 遍历
}
std::cout << std::endl;
// 访问元素
std::cout << "第一个元素: " << vec[0] << std::endl;
std::array 与 std::vector 的差异:
内存分配与生命周期:
容量与灵活性:
性能对比:
使用场景:
2.5 C++中的类型转换
在C++中,类型转换是一个重要的主题,尤其是在涉及到不同类型之间的转换时。由于C++引入了多种类型转换运算符(如static_cast、dynamic_cast、const_cast和reinterpret_cast),使得类型转换不仅更加灵活,而且在许多情况下更安全。
C 语言类型转换的风险:
ini
double pi = 3.1415;
int n = (int)pi; // C风格强制转换,可能丢失精度
在这个例子中,double类型的pi被转换为int类型,导致小数部分丢失。
C++类型转换运算符:
- static_cast:用于在类型之间进行静态转换,编译时检查类型安全。适合于大多数常见的类型转换,如基本数据类型之间的转换、类层次结构中的上行和下行转换。
ini
double pi = 3.1415;
int m = static_cast<int>(pi); // 安全的类型转换
- const_cast:用于添加或移除对象的const属性。这在需要修改一个原本是const的对象时,可以使用这个转换运算符。注意,使用const_cast来改变const对象的值是未定义行为。
ini
const int a = 10;
int* p = const_cast<int*>(&a); // 移除const属性
- reinterpret_cast:用于进行低级别的重新解释转换,通常用于不同类型之间的指针转换。使用时需要小心,因为它不会进行任何类型安全检查,可能导致程序不稳定或崩溃。
ini
float f = 3.14;
int bits = reinterpret_cast<int&>(f); // 危险的操作,不建议使用
- dynamic_cast:用于在类层次结构中进行安全的下行转换。它会在运行时检查对象的类型,如果转换失败,返回nullptr(对于指针)或抛出std::bad_cast异常(对于引用)。
ini
class Base {};
class Derived : public Base {};
Base* base = new Derived();
Derived* derived = dynamic_cast<Derived*>(base); // 安全的下行转换
if (derived) {
// 转换成功
}
三、C++的输入输出
C++提供了强大的输入输出(I/O)机制,通过标准库中的流(streams)来处理数据的输入和输出。这一章将详细介绍C++输入输出的基本概念,以及如何使用标准输入输出流和文件流。
3.1 输入输出流的基本概念
在C++中,输入输出流是处理数据输入和输出的主要机制。输入流用于从外部设备或文件读取数据,而输出流则用于将数据写入外部设备或文件。
- std::cin:标准输入流,通常连接到键盘,用于读取输入数据。
- std::cout:标准输出流,通常连接到显示器,用于输出数据。
- std::cerr:标准错误输出流,用于输出错误信息。
- std::clog:标准日志输出流,用于输出日志信息。
使用 std::cout 输出数据:
c
#include <iostream>
int main() {
std::cout << "Hello, World!" << std::endl; // 输出字符串并换行
int num = 42;
std::cout << "The answer is: " << num << std::endl; // 输出整数
return 0;
}
使用 std::cin 输入数据:
c
#include <iostream>
int main() {
int age;
std::cout << "Enter your age: ";
std::cin >> age; // 从标准输入读取整数
std::cout << "You are " << age << " years old." << std::endl;
return 0;
}
使用std::cin时,可能会遇到输入错误。可以使用std::cin.fail()来检查输入是否成功:
c
#include <iostream>
#include <limits>
int main() {
int age;
std::cout << "Enter your age: ";
while (!(std::cin >> age)) {
std::cin.clear(); // 清除错误状态
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 丢弃无效输入
std::cout << "Invalid input. Please enter a number: ";
}
std::cout << "You are " << age << " years old." << std::endl;
return 0;
}
3.2 文件输入输出
C++提供了文件输入输出的功能,可以通过文件流来读写文件。
文件流包括std::ifstream(输入文件流)和std::ofstream(输出文件流)。使用这些流可以方便地从文件读取数据或向文件写入数据。
c
#include <iostream>
#include <fstream>
int main() {
std::ofstream outFile("output.txt"); // 创建输出文件流
if (outFile.is_open()) {
outFile << "Hello, File!" << std::endl; // 写入文件
outFile.close(); // 关闭文件
} else {
std::cerr << "Unable to open file for writing." << std::endl;
}
std::ifstream inFile("output.txt"); // 创建输入文件流
if (inFile.is_open()) {
std::string line;
while (std::getline(inFile, line)) { // 逐行读取文件
std::cout << line << std::endl; // 输出到控制台
}
inFile.close(); // 关闭文件
} else {
std::cerr << "Unable to open file for reading." << std::endl;
}
return 0;
}
二进制文件操作:
c
// 二进制写入
struct Data { int id; double value; };
Data d{1001, 3.14};
std::ofstream binOut("data.bin", std::ios::binary);
binOut.write(reinterpret_cast<char*>(&d), sizeof(Data));
// 二进制读取
Data d2;
std::ifstream binIn("data.bin", std::ios::binary);
binIn.read(reinterpret_cast<char*>(&d2), sizeof(Data));
3.3 文件打开模式
在打开文件时,可以指定不同的打开模式,主要有以下几种:
- std::ios::in:打开文件用于输入(读取)。
- std::ios::out:打开文件用于输出(写入),如果文件已存在则清空。
- std::ios::app:以追加模式打开文件,写入数据时不会清空原有内容。
- std::ios::binary:以二进制模式打开文件,适用于非文本文件。
c
std::ofstream outFile("output.txt", std::ios::app); // 以追加模式打开
3.4 格式化输出
C++的标准库支持格式化输出,使用std::ios提供的成员函数可以控制输出格式。
设置精度:
c
#include <iomanip>
std::cout << std::fixed << std::setprecision(2) << 3.14159 << std::endl; // 输出为3.14
设置宽度:
c
std::cout << std::setw(10) << std::setfill('*') << 42 << std::endl; // 输出为"********42"
进制转换:
c
// 进制转换
std::cout << std::hex << 255; // ff
std::cout << std::oct << 64; // 100
四、C++的命名空间
4.1 C语言的痛点
命名空间(Namespace)是C++提供的一种组织代码的机制,用于解决命名冲突的问题。通过将标识符(如变量、函数、类等)放置在命名空间中,可以将它们分组,以便于代码管理和重用。本章将详细介绍C++中的命名空间及其使用。
如果是C代码:
arduino
// 第三方库A
void log(const char* msg) { /* 写日志文件 */ }
// 第三方库B
void log(int code) { /* 发送网络日志 */ }
// 开发者代码
log("Error"); // 编译错误:函数重载冲突
C的解决方案及其缺陷:
arduino
// 通过前缀解决(笨拙且冗长)
void libA_log(const char* msg);
void libB_log(int code);
C++ 的解决方案:
arduino
namespace LibA {
void log(const char* msg) { /*...*/ }
}
namespace LibB {
void log(int code) { /*...*/ }
}
LibA::log("Message"); // 明确作用域
LibB::log(500); // 消除歧义
4.2 命名空间的定义与使用
名空间是一种作用域,用于包含标识符,从而避免命名冲突。C++中的命名空间可以包含函数、变量、类型、类、枚举等。
定义与成员访问 :
arduino
namespace Graphics {
const double PI = 3.14159;
class Circle {
double radius;
public:
Circle(double r) : radius(r) {}
double area() const { return PI * radius * radius; }
};
}
// 访问方式
Graphics::Circle c(5.0);
std::cout << Graphics::PI;
使用命名空间中的标识符时,可以通过以下两种方式访问:
- 使用作用域运算符(::):
arduino
NamespaceName::identifier;
- 使用using声明:
arduino
using NamespaceName::identifier;
- 使用using namespace指令
arduino
using namespace NamespaceName;
其实我们在使用标准库的时候就已经用到过命名空间:
c
#include <iostream>
int main() {
std::cout << "Hello, World!" << std::endl; // 使用std命名空间的cout
return 0;
}
定义用户命名空间与函数
c
namespace MyNamespace {
void myFunction() {
std::cout << "This is my function in MyNamespace." << std::endl;
}
}
//使用
#include <iostream>
namespace MyNamespace {
void myFunction() {
std::cout << "This is my function in MyNamespace." << std::endl;
}
}
int main() {
MyNamespace::myFunction(); // 调用自定义命名空间中的函数
return 0;
}
4.3 嵌套命名空间
C++支持命名空间的嵌套,即在一个命名空间内定义另一个命名空间。
c
namespace OuterNamespace {
namespace InnerNamespace {
void innerFunction() {
std::cout << "This is a function in InnerNamespace." << std::endl;
}
}
}
int main() {
OuterNamespace::InnerNamespace::innerFunction(); // 调用嵌套命名空间中的函数
return 0;
}
命名空间是一种有效的代码组织方式,可以提高代码的可读性和可维护性。通过合理使用命名空间,可以有效地避免命名冲突,组织大型项目中的代码结构。
我们需要注意的是:
- 避免全局命名空间污染:尽量使用命名空间来组织代码,避免在全局命名空间中定义过多的标识符。
- 避免重复定义:如果在不同的命名空间中定义同名的标识符,确保在使用时能够明确对应的命名空间。
- 不推荐使用using namespace:在头文件中使用using namespace可能导致命名冲突,建议在源文件中使用、或使用局部的using声明。
除此之外一些高版本的还会有内联命名空间、命名空间别名、匿名命名空间等特性,用的不多这里就不扩展了。
五、C++的类与对象
C++是面向对象的编程语言,类和对象是其核心概念。类是对象的蓝图,而对象是类的实例。本章将深入探讨C++中的类与对象的基本特性、使用方式及其相关概念。
5.1 类的概念
类是用户定义的数据类型,封装了数据和对数据的操作方法。类的基本语法如下:
kotlin
class ClassName {
public:
// 成员变量
// 成员函数
};
这一点相信会 Java 都特别能理解。
5.2 对象的创建与使用
对象是类的实例,通过类的构造函数创建。对象可以访问类中的成员变量和成员函数。
c
class Dog {
public:
void bark() {
std::cout << "Woof!" << std::endl;
}
};
int main() {
Dog myDog; // 创建对象
myDog.bark(); // 调用成员函数
return 0;
}
这里需要扩展一下对象的创建方式以及堆栈内存的用法:
简单的栈中创建对象,自动管理自动回收:
arduino
#include <iostream>
#include <stdlib.h>
using namespace std;
class TV
{
public:
char name[20];
int type;
void changeVol();
void power();
};
int main(void)
{
TV tv;//定义一个对象
// TV tv[20]; //定义一个对象数组
tv.type = 0; //栈实例化出来的对象使用.进行对象成员访问。
tv.changeVol();
return 0;
}//从栈中实例化对象 会自动回收
灵活的堆中创建对象,手动管理:
ini
int main(void)
{
TV *p = new TV(); //在堆中实例化一个对象,new运算符申请出来的内存就是在堆上的了。
// TV *q = new TV[20]; // 定义一个对象数组
p -> type = 0;//堆实例化出来的对象使用->进行对象成员访问。
p -> changeVol();
delete p;
// delete []q;
return 0;
}//从堆中实例化对象
不管是对象还是数组我们需要手动的 delete 释放内存。
当然你要说我可以用智能指针,那也是可以的
c
std::unique_ptr<TV> p = std::make_unique<TV>();
// 无需手动delete,超出作用域自动释放
方法有很多种。
5.3 构造函数与构析函数
构造函数是用于初始化对象的特殊成员函数,而析构函数在对象生命周期结束时被调用,用于释放资源。
构造函数:
- 默认构造函数:无参数的构造函数。
- 带参数的构造函数:用于初始化成员变量。
c
class Person {
public:
std::string name;
int age;
Person() : name("Unknown"), age(0) {} // 默认构造函数
Person(std::string n, int a) : name(n), age(a) {} // 带参数的构造函数
};
这一点很好理解,构析函数就是Java没有的概念,用于对象要被回收的时候调用的,用于释放资源,也是很方便很常用:
析构函数的名称与类名相同,但前面加~符号,无法带参数。
c
class Resource {
public:
Resource() {
std::cout << "Resource acquired." << std::endl;
}
~Resource() {
std::cout << "Resource released." << std::endl;
}
};
5.4 访问权限控制
C++提供三种访问控制修饰符,用于控制类成员的可访问性:
- public:公共成员,可以在任何地方访问。
- protected:受保护成员,子类可以访问,但外部代码不能。
- private:私有成员,只有类内部可以访问,外部代码无法访问。
arduino
class Example {
public:
int publicVar;
protected:
int protectedVar;
private:
int privateVar;
};
当然了,也不是绝对的,比如就算是 private 的,但是我们定义了友元函数一样可以突破:
arduino
class BankAccount {
private:
double balance;
friend class BankManager; // 授权友元类
friend void auditAccount(BankAccount&); // 授权友元函数
};
class BankManager {
void resetBalance(BankAccount& acc) {
acc.balance = 0; // 访问私有成员
}
};
5.5 继承与多态
继承是面向对象编程的一个重要特性,可以通过继承创建新类(子类)来扩展现有类(基类)的功能。
c
class Base {
public:
void display() {
std::cout << "Base class" << std::endl;
}
};
class Derived : public Base { // 公共继承
public:
void show() {
std::cout << "Derived class" << std::endl;
}
};
继承的访问控制
- 公有继承:基类的公有成员在派生类中仍然是公有的,受保护的成员在派生类中仍然是受保护的,私有成员在派生类中不可访问。
- 保护继承和私有继承的规则稍有不同,受保护和私有成员的可见性会有所变化。
多态是指同一操作作用于不同对象能产生不同效果的能力。C++通过虚函数实现运行时多态。
c
class Base {
public:
virtual void speak() {
std::cout << "Base speaking." << std::endl;
}
};
class Derived : public Base {
public:
void speak() override {
std::cout << "Derived speaking." << std::endl;
}
};
int main() {
Base* ptr = new Derived();
ptr->speak(); // 输出 "Derived speaking."
delete ptr;
return 0;
}
5.6 运算符重载
运算符重载是C++的一项重要特性,它允许程序员为自定义类型(类)定义特定的运算符行为。这使得类的对象可以使用运算符进行操作,像内置类型一样自然。
运算符重载使用关键字 operator。重载运算符可以是成员函数或非成员函数(友元函数)。
通过重载运算符,可以定制使用运算符(如 +, -, *, == 等)时的行为,使其适用于用户定义的类。
这里演示一下给一个自定义类添加一个 + 的运算操作:
csharp
class Complex {
public:
double real, imag;
Complex(double r, double i) : real(r), imag(i) {}
// 重载加法运算符
Complex operator+(const Complex& other) {
return Complex(real + other.real, imag + other.imag);
}
};
// 使用示例
int main() {
Complex c1(1.0, 2.0);
Complex c2(3.0, 4.0);
Complex c3 = c1 + c2; // 使用重载的 + 运算符
return 0;
}
5.7 静态成员
静态成员变量在类的所有实例中共享。它们通常用于跟踪类的状态或计数器。
c
class Student {
public:
static int count; // 声明静态成员变量
Student() {
count++; // 每创建一个对象,计数器加一
}
};
// 在类外定义静态成员变量
int Student::count = 0;
// 使用示例
int main() {
Student s1;
Student s2;
std::cout << "Number of students: " << Student::count << std::endl; // 输出2
return 0;
}
与之对应的是静态成员函数,它只能访问静态成员变量,因为它们没有 this 指针。它们可以被类的对象直接调用,也可以通过类名调用。
c
class Math {
public:
static int add(int a, int b) {
return a + b;
}
};
// 使用示例
int main() {
int result = Math::add(3, 4); // 使用类名调用静态成员函数
std::cout << "Sum: " << result << std::endl; // 输出7
return 0;
}
六、C++的函数
函数是C++编程的基本构建块,允许程序员将代码组织成可重用的块。函数可以是类的一部分(成员函数),也可以是全局可用的(非成员函数)。在本章中,我们将探讨C++中的不同类型的函数及其特性。
6.1 类的成员函数
成员函数是定义在类内部的函数,用于操作类的成员变量。它们可以访问类的私有和公有成员。
arduino
class Rectangle {
public:
double width, height;
// 成员函数
double area() {
return width * height;
}
};
// 使用示例
int main() {
Rectangle rect;
rect.width = 5.0;
rect.height = 10.0;
std::cout << "Area: " << rect.area() << std::endl; // 输出 "Area: 50"
return 0;
}
其中的成员函数还分为友元函数,它是一个可以访问类的私有和保护成员的非成员函数。通过将函数声明为友元,类允许它访问其内部实现。这在某些情况下非常有用,例如,当需要在类外实现某些与类紧密相关的操作时。
arduino
class Box {
private:
double width;
public:
Box(double w) : width(w) {}
// 友元函数的声明
friend void printWidth(Box box);
};
// 友元函数的定义
void printWidth(Box box) {
std::cout << "Width: " << box.width << std::endl;
}
// 使用示例
int main() {
Box b(10);
printWidth(b); // 输出 "Width: 10"
return 0;
}
6.2 非成员函数
非成员函数是定义在类外的函数,它们不能直接访问类的私有成员。不过,非成员函数可以通过类的公有接口来操作类的对象。
c
class Circle {
public:
double radius;
};
// 非成员函数
double calculateArea(Circle c) {
return 3.14159 * c.radius * c.radius;
}
// 使用示例
int main() {
Circle c;
c.radius = 5.0;
std::cout << "Area: " << calculateArea(c) << std::endl; // 输出 "Area: 78.53975"
return 0;
}
6.3 静态成员函数
静态成员函数属于类而不是类的对象,它们不能访问非静态成员。静态成员函数只能访问静态成员变量和调用其他静态成员函数。
这个在类的篇幅中也有提到:
c
class Counter {
public:
static int count; // 静态成员变量
Counter() {
count++;
}
// 静态成员函数
static int getCount() {
return count;
}
};
// 定义静态成员变量
int Counter::count = 0;
// 使用示例
int main() {
Counter c1, c2;
std::cout << "Count: " << Counter::getCount() << std::endl; // 输出 "Count: 2"
return 0;
}
6.4 函数的重载
函数重载允许在同一作用域中使用相同的函数名称,但参数类型或参数个数不同。编译器根据函数调用时提供的参数来决定调用哪个函数。
c
class Print {
public:
void show(int i) {
std::cout << "Integer: " << i << std::endl;
}
void show(double d) {
std::cout << "Double: " << d << std::endl;
}
void show(const std::string& s) {
std::cout << "String: " << s << std::endl;
}
};
// 使用示例
int main() {
Print p;
p.show(5); // 调用第一个重载
p.show(5.5); // 调用第二个重载
p.show("Hello"); // 调用第三个重载
return 0;
}
和 Java 类的重载方法类似,很容易理解。
6.5 函数的默认参数
在函数定义时,可以为参数提供默认值,这样在调用函数时可以省略某些参数。未提供的参数将使用默认值。
c
class Math {
public:
// 函数带有默认参数
int add(int a, int b = 0) {
return a + b;
}
};
// 使用示例
int main() {
Math m;
std::cout << "Sum with one parameter: " << m.add(5) << std::endl; // 输出 "Sum with one parameter: 5"
std::cout << "Sum with two parameters: " << m.add(5, 10) << std::endl; // 输出 "Sum with two parameters: 15"
return 0;
}
其实 C语言就有的东西。
6.6 内联函数
内联函数是使用 inline 关键字定义的,它们的实现会在每次调用时插入到调用点。这可以提高性能,但可能会增加代码大小。
c
class Math {
public:
// 定义内联函数
inline int square(int x) {
return x * x;
}
};
// 使用示例
int main() {
Math m;
std::cout << "Square: " << m.square(5) << std::endl; // 输出 "Square: 25"
return 0;
}
其实 Klotin 就有这个概念,也是类似的东西
6.7 Lambda 表达式
Lambda 表达式是C++11引入的一种简洁的方式,用于定义匿名函数。它们可以捕获外部作用域中的变量,使得在需要传递函数作为参数时变得更加方便。
Lambda 表达式的基本语法:
scss
[capture](parameters) -> return_type { function_body }
- capture:指定捕获外部变量的方式。
- parameters:参数列表。
- return_type:返回类型(可省略,编译器可以推断)。
- function_body:函数体。
示例:
c
[](int n) {
std::cout << n << " ";
}
- 捕获:这个 Lambda 表达式没有捕获任何外部变量,使用了空的捕获列表 []。
- 参数:它接受一个整数参数 n。
- 逻辑:它的功能是将传入的参数 n 打印出来,没有返回值。这是一个简单的函数接口,主要用于输出。
再来一个有返回值的:
arduino
[](int n) {
return n > 3;
- 捕获:此 Lambda 也没有捕获任何外部变量。
- 参数:同样接受一个整数参数 n。
- 逻辑:它的功能是判断 n 是否大于 3。如果是,返回 true;否则返回 false。这是一个条件检查的 Lambda 表达式,通常用于过滤或条件判断。
按值捕获外部变量并返回的 Lambda 表达式:
ini
int x = 10;
auto lambda = [x]() { return x + 1; }; // 按值捕获
- 捕获:这个 Lambda 捕获了外部变量 x,使用 [] 中的 x 指出这是按值捕获。
- 逻辑:在 Lambda 的函数体中,它对 x 进行了加 1 操作并返回结果。由于使用了按值捕获,Lambda 内部的 x 是外部 x 的一个副本,因此对其所做的任何修改不会影响外部的 x 的值。在这个例子中,它的返回值将是 11,而外部的 x 仍然是 10。
终极示例捕获+参数+返回逻辑:
c
#include <iostream>
int main() {
int x = 10;
// 创建 Lambda 表达式,捕获外部变量 x,并接受一个参数 y
auto lambda = [x](int y) {
return x + y; // 返回 x 和 y 的和
};
// 调用 Lambda 表达式,传入参数 y
int result1 = lambda(5); // 传入 5,计算 10 + 5
int result2 = lambda(3); // 传入 3,计算 10 + 3
std::cout << "Result of lambda with y = 5: " << result1 << std::endl; // 输出 15
std::cout << "Result of lambda with y = 3: " << result2 << std::endl; // 输出 13
return 0;
}
是不是很容易就懂了呢。
在标准库的很多API方法都支持这种表达式了:
c
auto isEven = [](int x) { return x%2 == 0; };
std::vector<int> nums {1,2,3,4};
std::copy_if(nums.begin(), nums.end(),
std::ostream_iterator<int>(std::cout, " "),
isEven); // 输出2 4
数组的操作也有:
c
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
// 使用 Lambda 表达式打印每个元素
std::for_each(numbers.begin(), numbers.end(), [](int n) {
std::cout << n << " ";
});
std::cout << std::endl;
// 使用 Lambda 表达式进行条件过滤
auto it = std::find_if(numbers.begin(), numbers.end(), [](int n) {
return n > 3; // 寻找大于3的第一个元素
});
if (it != numbers.end()) {
std::cout << "First number greater than 3: " << *it << std::endl; // 输出 "First number greater than 3: 4"
}
return 0;
}
6.8 模板函数
模板函数允许编写与类型无关的代码。通过模板,函数可以处理任何数据类型,而不需要为每种类型编写单独的函数。
这个我在下在下一章节中详细说明。
七、C++的模板
7.1 为什么要模版
C 语言的痛点:
比如要做一个不同类型交换的函数
css
/ 整型交换
void swap_int(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
/ 浮点型交换
void swap_float(float* a, float* b) { /* 相同实现 */ }
动态类型缺失,C语言方案:使用void*牺牲类型安全:
arduino
// 通用但危险的实现
void swap(void* a, void* b, size_t size) {
char temp[size];
memcpy(temp, a, size);
memcpy(a, b, size);
memcpy(b, temp, size);
}
// 使用时可能出错
double x = 3.14, y = 2.71;
swap(&x, &y, sizeof(int)); // 错误的大小参数!
痛点:代码冗余,修改逻辑需同步多个函数,复用性差,动态类型缺失,无编译期类型检查,复用性差
C++ 中的模板是一个强大的特性,允许程序员编写与类型无关的代码。通过模板,可以创建通用的函数和类,从而提高代码的重用性和可维护性。
7.2 函数模板
函数模板允许定义通用的函数,可以接受不同类型的参数。通过使用模板参数,编译器在调用函数时生成特定类型的版本。
定义与使用模板:
c
#include <iostream>
template <typename T>
T add(T a, T b) {
return a + b;
}
int main() {
std::cout << add(5, 3) << std::endl; // 整数相加
std::cout << add(5.5, 2.2) << std::endl; // 浮点数相加
return 0;
}
其实我们可以指定参数,也可以自动推导参数
csharp
// 方案一:不写明,计算机会自动进行识别
int ival = add(100,99); //模板函数
//方案二:指定之后,传入参数一定要是这种数据类型才可以
char cval = add<char>('A','B');
这样也是可以的,同时我们的模板还能指定参数的引用(这一点后面讲):
ini
template <typename T>
void swap(T& a,T& b)
{
T tmp = a;
a = b;
b = tmp;
}
int x =20,y=30;
swap<int>(x,y);
7.3 类模板
类模板允许定义通用的类,可以使用不同的数据类型创建类的实例。它提供了一种在类中处理多种数据类型的方式。
c
#include <iostream>
template <typename T>
class Box {
public:
Box(T value) : value(value) {}
T getValue() const { return value; }
private:
T value;
};
int main() {
Box<int> intBox(123); // 创建一个整数盒子
Box<std::string> strBox("Hello"); // 创建一个字符串盒子
std::cout << intBox.getValue() << std::endl; // 输出123
std::cout << strBox.getValue() << std::endl; // 输出Hello
return 0;
}
不管是函数模板还是类模板都是可以指定Class类作为参数的,并且还能混用:
arduino
template <typename T,class U>
T minus(T* a, U b)
template <typename T,int size>
void display(T a)
{
for(int i=0;i < size;i++)
cout << a << endl;
}
display<int,5>(15);
类模板:
arduino
template <class T>
// 类名后面写<T>
void MyArray<T>::display()
{
}
//使用
int main()
{
MyArray<int> arr;
arr.display();
return 0;
}
总结:C++模板是一种强大的特性,通过提供代码的通用性和类型安全性,提高了代码的重用性和维护性。模板的使用不仅简化了代码的编写过程,还优化了性能。
八、C++的指针与引用以及内存管理
8.1 C++ 的指针
指针是一个存储变量地址的变量。在 C++ 中,指针的基本概念与 C 语言相似,仍然使用 * 来声明指针,使用 & 来获取变量的地址。
c
#include <iostream>
int main() {
int a = 10;
int* ptr = &a; // ptr 是一个指向整型的指针,指向变量 a 的地址
std::cout << "Value of a: " << a << std::endl; // 输出 10
std::cout << "Address of a: " << &a << std::endl; // 输出 a 的地址
std::cout << "Value at ptr: " << *ptr << std::endl; // 输出 10,通过指针访问 a 的值
return 0;
}
同时 C++ 对指针有一些新特性扩展。
比如 C++ 支持指向类成员函数的指针,这在类中进行回调和事件处理时非常有用。
c
#include <iostream>
class MyClass {
public:
void display() {
std::cout << "Hello from MyClass!" << std::endl;
}
};
int main() {
MyClass obj;
void (MyClass::*funcPtr)() = &MyClass::display; // 指向成员函数的指针
(obj.*funcPtr)(); // 调用成员函数
return 0;
}
成员指针(member pointer)允许指向类中的非静态成员变量。
c
#include <iostream>
class MyClass {
public:
int value;
MyClass(int val) : value(val) {}
};
int main() {
MyClass obj(42);
int MyClass::*ptr = &MyClass::value; // 指向 MyClass 中的成员变量 value
std::cout << "Value: " << obj.*ptr << std::endl; // 访问成员变量
return 0;
}
这些两点都是关于类的一些扩展,因为 C 语言没有类这个概念,所以这算是新特性。
8.2 C++ 智能指针
智能指针是 C++11 引入的一种自动管理内存的机制,主要包括 std::unique_ptr、std::shared_ptr 和 std::weak_ptr。
- std::unique_ptr:独占所有权,确保一个时间内只有一个指针指向该内存地址。
- std::shared_ptr:允许多个指针共享同一资源,通过引用计数管理内存。
- std::weak_ptr:与 std::shared_ptr 配合使用,避免循环引用的问题。
c
#include <iostream>
#include <memory>
class Resource {
public:
Resource() { std::cout << "Resource acquired" << std::endl; }
~Resource() { std::cout << "Resource released" << std::endl; }
};
int main() {
{
std::unique_ptr<Resource> res1(new Resource()); // 使用 unique_ptr
// std::unique_ptr<Resource> res2 = res1; // 错误,不能拷贝
} // 资源在离开作用域时自动释放
{
std::shared_ptr<Resource> res1 = std::make_shared<Resource>(); // 使用 shared_ptr
{
std::shared_ptr<Resource> res2 = res1; // 共享所有权
std::cout << "Shared ownership" << std::endl;
} // res2 离开作用域,资源计数减少
} // 资源在最后一个 shared_ptr 离开作用域时释放
return 0;
}
智能指针在资源在离开作用域时自动释放,可以避免我们调用 delete 等API去手动设置。
8.3 引用的概念
引用是 C++ 中一种特殊的类型,它是对变量的别名。引用必须在声明时初始化,并且一旦绑定到某个变量后就无法改变。
c
#include <iostream>
int main() {
int a = 10;
int& ref = a; // ref 是 a 的引用
std::cout << "Value of a: " << a << std::endl; // 输出 10
std::cout << "Value of ref: " << ref << std::endl; // 输出 10
ref = 20; // 修改引用会影响原变量
std::cout << "Value of a after modification: " << a << std::endl; // 输出 20
return 0;
}
除了数据类型,类可以引用,其实指针也能有引用:
ini
#include <iostream>
using namespace std;
int main(void)
{
int a = 10;
int *p = &a; // 定义指针p
int *&q = p; // 指针p的别名q
*q = 20;
cout << a << endl;
system("pause");
return 0;
}
引用与指针的对比:
- 语法简洁性:引用在使用时不需要解引用操作符 *,因此语法更简洁。
- 内存管理:引用不能为 nullptr,更安全;指针可以为空,需要额外处理。
- 重新指向:引用一旦被初始化后不能再指向其他变量,而指针可以随时更改指向。
引用常用于函数参数传递和返回值,避免大对象的复制开销。
c
#include <iostream>
void modify(int& value) { // 引用作为参数
value += 10;
}
int main() {
int num = 5;
modify(num); // num 通过引用传递
std::cout << "Modified value: " << num << std::endl; // 输出 15
return 0;
}
引用也支持链式调用:
typescript
Logger& Logger::log(const std::string& msg) {
// 记录日志...
return *this;
}
8.4 C++ 的内存管理
在 C 语言中,动态内存管理通过 malloc 和 free 函数实现。这种管理方法需要程序员手动进行内存分配与释放,容易出现内存泄漏和悬挂指针的问题。
c
#include <stdlib.h>
int main() {
int* arr = (int*)malloc(10 * sizeof(int)); // 动态分配内存
// 使用 arr
free(arr); // 手动释放内存
return 0;
}
C++ 提供了 new 和 delete 运算符用于动态内存管理,这些运算符在内存分配时调用构造函数,在释放内存时调用析构函数。
c
#include <iostream>
class MyClass {
public:
MyClass() { std::cout << "Constructor called" << std::endl; }
~MyClass() { std::cout << "Destructor called" << std::endl; }
};
int main() {
MyClass* obj = new MyClass(); // 使用 new 分配内存
delete obj; // 使用 delete 释放内存
return 0;
}
智能指针是 C++11 引入的一种内存管理工具,它提供了自动释放内存的功能,减少了手动管理带来的风险。
c
#include <iostream>
#include <memory>
int main() {
std::unique_ptr<int> ptr(new int(10)); // 创建 unique_ptr
std::cout << "Value: " << *ptr << std::endl;
// 不需要手动 delete,ptr 超出作用域后自动释放内存
return 0;
}
这个也是讲了很多次了。
std::shared_ptr 允许多个指针共享同一资源,并使用引用计数来管理内存。
c
#include <iostream>
#include <memory>
void sharedPtrExample() {
std::shared_ptr<int> ptr1 = std::make_shared<int>(20);
std::shared_ptr<int> ptr2 = ptr1; // 共享所有权
std::cout << "Value: " << *ptr2 << std::endl;
} // ptr1 和 ptr2 超出作用域后,内存自动释放
int main() {
sharedPtrExample();
return 0;
}
std::weak_ptr 用于解决 std::shared_ptr 的循环引用问题。它提供了一种观察但不拥有资源的方式。
c
#include <iostream>
#include <memory>
class Node {
public:
std::shared_ptr<Node> next;
~Node() { std::cout << "Node destroyed" << std::endl; }
};
int main() {
std::shared_ptr<Node> first(new Node());
std::weak_ptr<Node> second = first; // weak_ptr 观察但不拥有
first->next = second.lock(); //.lock() 获取 shared_ptr
return 0;
} // 退出作用域时,first 被销毁,内存自动释放
综合示例:
c
class Database {
std::shared_ptr<Connection> conn;
public:
explicit Database(const std::string& url)
: conn(std::make_shared<Connection>(url)) {}
QueryResult query(const std::string& sql) {
return conn->execute(sql);
}
};
void processUserData() {
auto db = std::make_shared<Database>("mysql://localhost");
auto analyzer = std::make_unique<DataAnalyzer>(db);
try {
auto report = analyzer->generateReport();
report.exportToFile("data.csv");
} catch (const std::exception& e) {
// 所有资源自动释放
}
}
总结
本文我们探讨了从 Java 到 C,再到 C++ 的学习过程,明确了这三种编程语言之间的区别与联系。通过对 C++ 特性的逐步介绍,我们不仅理解了面向对象编程的核心概念,还掌握了 C++ 中的模板、标准模板库、异常处理、命名空间等重要特性。这些特性不仅提升了程序的复用性和灵活性,还使得代码的组织更为高效。
我们首先分析了 C 语言的基本特性,认识到它的简单性和高效性在现代编程中的重要性,但同时也意识到它在内存管理和错误处理上的局限。随后,转向 C++,我们探讨了它是如何在 C 的基础上进行扩展,引入了类、对象和运算符重载等特性。
在内存管理方面,C++ 提供了智能指针的概念,极大地简化了动态内存的管理,减少了内存泄漏和悬挂指针的风险。同时,C++ 的模板机制使得我们能够编写类型无关的代码,从而提高代码的重用性。
总之 C++ 继承了 C 的很多特性,它通过面向对象的编程方法和现代化的编程理念,为开发者提供了更强大的工具。掌握 C++ 的特性,不仅为复杂项目的开发奠定了基础,也为程序员在计算机科学领域的进一步探索铺平了道路。
如果大家兴趣也可以关注一下后期的编译相关的文章哦,后期不定期我会继续厚颜发一些相关的学习笔记。
如果我的文章有错别字,不通顺的,或者代码、注释、有错漏的地方,同学们都可以指出修正。
如果感觉本文对你有一点的启发和帮助,还望你能点赞
支持一下,你的支持对我很重要!
Ok,那么这一期就此完结了。