C++基础知识与核心概念精讲
C++是一种高级编程语言,它以C语言为基础,同时引入了面向对象编程(OOP)的特性。自1983年由Bjarne Stroustrup在贝尔实验室首次开发以来,C++凭借其强大的性能和灵活性成为了系统软件、游戏开发、实时物理模拟等领域中不可或缺的工具。C++语言支持多种编程范式,包括过程化编程、面向对象编程和泛型编程。这意味着程序员可以根据具体问题选择最适合的编程方法。C++支持复杂的抽象机制,如类
简介:C++是一种高效的编程语言,适用于多种软件开发领域。本书详细阐述了C++的基础语法、函数、内存管理、指针、面向对象编程、模板、标准库和异常处理等核心概念,并通过丰富的示例和练习,帮助初学者建立坚实的基础。学习C++不仅能够掌握一种强大的编程工具,还将理解计算机科学的核心原理,为IT领域的职业生涯打下坚实基础。 
1. C++编程语言概述
C++是一种高级编程语言,它以C语言为基础,同时引入了面向对象编程(OOP)的特性。自1983年由Bjarne Stroustrup在贝尔实验室首次开发以来,C++凭借其强大的性能和灵活性成为了系统软件、游戏开发、实时物理模拟等领域中不可或缺的工具。
C++语言支持多种编程范式,包括过程化编程、面向对象编程和泛型编程。这意味着程序员可以根据具体问题选择最适合的编程方法。C++支持复杂的抽象机制,如类和继承,以及模板,允许开发者编写类型安全的代码。
随着技术的发展,C++不断地进化以满足新的编程需求。2011年,C++11标准的发布标志着C++语言的一个重要里程碑,它引入了大量新特性,比如自动类型推导、移动语义、统一初始化器、智能指针等,使得C++更加强大和易用。
#include <iostream>
using namespace std;
int main() {
cout << "Hello, C++ World!" << endl;
return 0;
}
在上述示例中,我们看到一个简单的C++程序,它包含了所有标准C++程序的基本元素:包含预处理指令 #include <iostream> 用于输入输出流,使用 using namespace std; 声明标准命名空间,以及一个主函数 main() ,它是任何C++程序的入口点。程序输出字符串”Hello, C++ World!”到标准输出流。
本章将继续介绍C++编程语言的基础知识,包括它的主要特点和对现代编程实践的影响。
2. C++基本语法理解
2.1 变量与数据类型
2.1.1 常量和变量的声明
在C++中,变量是用于存储数据的命名位置,其值在程序执行期间可能会改变。声明变量时,需要指定其数据类型和名称。例如, int number = 42; 声明了一个名为 number 的整型变量,并将其初始化为 42。常量则是存储在程序中,其值在编译后不会改变的量,使用 const 关键字声明。例如, const int maxUsers = 100; 声明了一个名为 maxUsers 的常量整型,并初始化为 100。
2.1.2 内建数据类型及其使用
C++ 提供了丰富的内建数据类型,包括整型(int)、浮点型(float、double)、字符型(char)、布尔型(bool)等。每种类型有不同的大小和取值范围。例如:
int integer = 1024;
float real = 3.14159;
char character = 'A';
bool boolean = true;
代码中的 int 用于存储整数, float 用于存储单精度浮点数, char 用于存储单个字符, bool 用于逻辑操作,其值只能是 true 或 false 。
2.2 控制结构
2.2.1 条件控制语句
条件控制语句允许程序根据条件表达式的结果决定执行不同的代码分支。在C++中,最常见的条件控制语句是 if 语句。基本的 if 语句结构如下:
if (condition) {
// 条件为真时执行的代码
} else {
// 条件为假时执行的代码
}
其中 condition 是一个返回布尔值的表达式。此外,还可以使用 else if 来处理多个条件分支。
2.2.2 循环控制语句
循环控制语句用于重复执行一段代码直到满足特定条件。C++ 提供了 while 、 do-while 和 for 循环:
// while循环
while (condition) {
// 当条件为真时重复执行的代码
}
// do-while循环
do {
// 至少执行一次,之后条件为真时重复执行的代码
} while (condition);
// for循环
for (int i = 0; i < 10; ++i) {
// 执行10次的代码
}
2.2.3 跳转语句
跳转语句允许程序跳转到代码的其他部分。在C++中, break 用于跳出最内层的循环或 switch 语句; continue 跳过当前循环的剩余部分,继续下一次循环; goto 可以跳转到同一函数内的标签位置,但使用需谨慎,以免造成代码混乱。
2.3 函数基础
2.3.1 函数声明与定义
函数是C++程序的基本组成单元,用于执行特定的任务。函数的声明通常包括返回类型、函数名和参数列表,而定义包括函数体。例如:
// 函数声明
int add(int a, int b);
// 函数定义
int add(int a, int b) {
return a + b;
}
2.3.2 参数传递机制
函数可以通过值传递、引用传递或指针传递参数。值传递会复制实参值给形参,不会影响实参;引用传递和指针传递则是将实参的地址传递给函数,函数内对形参的修改会影响到实参。引用传递使用 & 符号,指针传递使用 * 符号。
// 值传递
int addValue(int a, int b) {
return a + b;
}
// 引用传递
int addRef(int& a, int& b) {
a += b;
return a;
}
// 指针传递
int addPtr(int* a, int* b) {
return *a + *b;
}
以上代码展示了三种不同的参数传递机制,各有其适用场景和优缺点。选择合适的参数传递方式对于提升程序性能和逻辑清晰度很重要。
在本章节的详细介绍中,我们深入理解了C++中变量和数据类型的声明与使用,掌握了条件和循环控制语句的正确用法,以及函数声明、定义和参数传递的细节。这些基础知识为学习更高级的编程概念奠定了坚实的基础。接下来的章节将探讨函数的高级特性,如函数重载、模板函数、函数指针和lambda表达式,以及这些特性如何在实际开发中发挥作用。
3. 函数的定义和使用
函数是C++编程中组织代码的基本构件,它们允许开发者封装一段可重复使用的代码块,通过参数传递数据,以实现特定的功能。本章节将深入探讨C++中函数的定义、重载、模板函数的使用,以及函数指针和lambda表达式等高级特性。
3.1 函数重载与模板函数
3.1.1 函数重载的规则和实例
函数重载允许存在多个同名函数,只要它们的参数列表不同即可。这种机制极大地增强了语言的表达能力,并允许程序员为不同的数据类型或者参数数量提供相同功能的函数。在C++中,函数重载的实现依赖于编译时的类型检查。
实例演示
假设我们想要为不同类型的数组提供一个打印函数,我们可以这样做:
#include <iostream>
using namespace std;
// 函数重载示例:打印整型数组
void printArray(const int arr[], int size) {
for (int i = 0; i < size; ++i) {
cout << arr[i] << " ";
}
cout << endl;
}
// 函数重载示例:打印浮点型数组
void printArray(const float arr[], int size) {
for (int i = 0; i < size; ++i) {
cout << arr[i] << " ";
}
cout << endl;
}
int main() {
int intArray[] = {1, 2, 3, 4, 5};
float floatArray[] = {1.1f, 2.2f, 3.3f, 4.4f, 5.5f};
printArray(intArray, 5);
printArray(floatArray, 5);
return 0;
}
3.1.2 模板函数的定义和应用
模板函数是C++泛型编程的核心,它允许以参数化的方式编写与数据类型无关的代码。使用模板函数可以避免重复代码,并提高代码的复用性。
实例演示
下面是一个模板函数 max 的定义,它可以比较任意类型的两个值,并返回较大的那个值。
#include <iostream>
#include <algorithm>
using namespace std;
// 模板函数定义
template <typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
int main() {
cout << "Max of int: " << max(2, 4) << endl;
cout << "Max of double: " << max(4.5, 5.6) << endl;
return 0;
}
在这个例子中, max 函数通过模板定义,可以接受任何类型的参数,并根据提供的参数类型来编译出相应的函数实例。
3.2 函数指针与lambda表达式
3.2.1 函数指针的概念和使用
函数指针是C++中的一种变量类型,它存储的是函数的内存地址。通过函数指针,我们可以在运行时动态地决定调用哪个函数,这样增加了代码的灵活性。
实例演示
#include <iostream>
using namespace std;
// 一个简单的函数
void simpleFunction() {
cout << "Function called." << endl;
}
int main() {
// 定义一个指向函数的指针
void (*funcPtr)() = simpleFunction;
// 通过函数指针调用函数
funcPtr();
return 0;
}
在这个例子中,我们定义了一个名为 simpleFunction 的函数,然后创建了一个指向该函数的指针 funcPtr ,通过 funcPtr 我们间接地调用了 simpleFunction 。
3.2.2 lambda表达式的引入和使用
Lambda表达式是C++11引入的一个强大特性,它允许开发者在需要函数对象的地方直接定义函数。Lambda表达式使得代码更加简洁,并且在处理闭包时显得尤为方便。
实例演示
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
int main() {
vector<int> v = {1, 2, 3, 4, 5};
// 使用lambda表达式进行排序
sort(v.begin(), v.end(), [](int a, int b) {
return a < b; // 降序排列
});
// 打印排序后的数组
for (int num : v) {
cout << num << " ";
}
cout << endl;
return 0;
}
在这个例子中,我们使用了一个lambda表达式来指定排序函数,通过比较两个元素的大小来实现降序排列。Lambda表达式的使用使得 sort 函数更加灵活,而无需单独定义一个比较函数。
通过本章节的介绍,读者应该对C++中的函数重载、模板函数、函数指针和lambda表达式的定义和使用有了深入的理解。这些高级特性使得函数在C++中非常灵活和强大,能够有效地提高代码的复用性和可维护性。
4. 内存管理技巧
4.1 动态内存管理
4.1.1 new和delete运算符的使用
在C++中,动态内存管理是通过指针和new、delete运算符来完成的。new运算符负责在堆(heap)上分配内存,而delete运算符则负责释放之前分配的内存。通过这种方式,程序员可以控制对象的生命周期。
int* p = new int(10); // 在堆上分配一个int类型的空间,并初始化为10
delete p; // 释放p指向的内存
上述代码演示了如何使用new和delete来动态分配和释放一个整数的空间。使用new时,通常会返回指向分配内存的指针。如果分配失败,new会抛出一个 std::bad_alloc 异常。
4.1.2 内存泄漏的防范
动态内存使用不当可能会导致内存泄漏,即分配的内存未被及时释放,长期占用导致资源耗尽。一个常见的防范策略是使用智能指针,如 std::unique_ptr 和 std::shared_ptr ,它们能够在适当的时候自动释放内存。
#include <memory>
std::unique_ptr<int> ptr = std::make_unique<int>(10); // 自动释放分配的内存
使用智能指针是现代C++编程中管理内存的推荐方式,它有助于提高代码的安全性和可维护性。
4.2 智能指针的运用
4.2.1 unique_ptr、shared_ptr和weak_ptr
C++11标准库中引入了三种智能指针: std::unique_ptr 、 std::shared_ptr 和 std::weak_ptr 。它们各自有不同的用途和行为。
unique_ptr
std::unique_ptr 是一个独占所有权的智能指针。当 unique_ptr 的实例销毁时,它所指向的对象也会被自动销毁。
std::unique_ptr<int> ptr = std::make_unique<int>(10);
ptr.reset(); // 释放资源
shared_ptr
std::shared_ptr 允许多个指针共享同一资源的所有权。资源会在最后一个 shared_ptr 被销毁时自动释放。
std::shared_ptr<int> ptr1 = std::make_shared<int>(10);
std::shared_ptr<int> ptr2 = ptr1; // 现在ptr1和ptr2共享资源
weak_ptr
std::weak_ptr 是一种不拥有对象的智能指针,它是为了打破 shared_ptr 可能产生的循环引用而设计的。当使用 weak_ptr 访问对象时,需要将其升级为 shared_ptr 。
std::shared_ptr<int> ptr = std::make_shared<int>(10);
std::weak_ptr<int> wp = ptr;
auto strong_wp = wp.lock(); // 如果资源可用,返回一个shared_ptr,否则返回nullptr
4.2.2 智能指针的常见用法和陷阱
智能指针虽然强大,但也存在一些使用陷阱。例如,对于 std::shared_ptr ,应当注意避免循环引用。循环引用是指两个或更多的 shared_ptr 相互引用,导致它们都无法被销毁,从而导致内存泄漏。
void func() {
std::shared_ptr<int> ptr1 = std::make_shared<int>(10);
std::shared_ptr<int> ptr2 = std::make_shared<int>(20);
ptr1->ptr2 = ptr2; // 循环引用的可能
ptr2->ptr1 = ptr1;
}
为了避免循环引用,可以使用 std::weak_ptr 来打破循环,或者调整对象的结构,确保它们不会相互拥有。
void func() {
std::shared_ptr<int> ptr1 = std::make_shared<int>(10);
std::weak_ptr<int> ptr2 = ptr1;
// ... 某些操作 ...
}
在实际应用中,智能指针可以极大地提高C++程序的稳定性和安全性,但需要正确理解和使用它们的特性。
5. 指针的深入探讨
指针是C++语言中一个复杂而又核心的概念,它涉及到内存地址的操作与管理,是深入理解C++内存模型和对象模型的关键。本章将带你深入理解指针在数组、函数以及类中的应用,并通过各种示例代码演示其强大的功能和灵活的使用场景。
5.1 指针与数组
5.1.1 指针与一维数组
在C++中,数组名本身就是数组首元素的地址,因此指针和数组之间有着天然的联系。通过指针可以遍历数组,也可以通过指针算术操作访问数组元素。以下代码演示了如何使用指针操作一维数组:
#include <iostream>
int main() {
int arr[] = {1, 2, 3, 4, 5};
int* ptr = arr; // 指针指向数组首地址
std::cout << "Array elements: ";
for (int i = 0; i < 5; ++i) {
std::cout << *(ptr + i) << " "; // 使用指针访问数组元素
}
std::cout << std::endl;
return 0;
}
在上述代码中, ptr 是一个指向整型的指针,通过 ptr + i 访问数组的第 i 个元素,这种方式称为指针算术。 *(ptr + i) 则是对该地址所指向的值的访问。
5.1.2 指针与多维数组
多维数组在内存中是线性存储的,这意味着多维数组可以使用一维数组的方式来处理。指针可以用于遍历多维数组的元素,但是需要更复杂的算术运算来定位元素的内存地址。
#include <iostream>
int main() {
int multiArr[2][3] = {{1, 2, 3}, {4, 5, 6}};
int (*ptr)[3] = multiArr; // 指针指向第一行首地址
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 3; ++j) {
std::cout << (*ptr)[j] << " "; // 使用指针访问多维数组元素
}
ptr++; // 移动到下一行的首地址
}
return 0;
}
在这个例子中, ptr 是一个指针,它指向一个包含3个整数的数组。通过递增 ptr ,我们可以在行间移动,而 (*ptr)[j] 则用于访问特定元素。
5.2 指针与函数
5.2.1 指针作为函数参数
指针可以作为函数的参数传递,这允许函数直接修改传入变量的值。这种机制通常称为“按引用传递”,使得函数更加灵活和强大。
#include <iostream>
void swap(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int x = 10, y = 20;
std::cout << "Before swap: x = " << x << ", y = " << y << std::endl;
swap(&x, &y); // 传递x和y的地址
std::cout << "After swap: x = " << x << ", y = " << y << std::endl;
return 0;
}
在这个例子中, swap 函数接受两个指向整数的指针作为参数,并交换了它们指向的值。注意我们传递的是变量的地址,而不是它们的值。
5.2.2 返回指针的函数
函数可以返回指针类型的数据,这允许函数返回指向动态分配内存、数组或字符串等的数据。返回指针需要谨慎,因为必须确保返回的内存块被正确管理。
#include <iostream>
int* createArray() {
int* arr = new int[10]; // 动态分配一个包含10个整数的数组
for (int i = 0; i < 10; ++i) {
arr[i] = i + 1;
}
return arr; // 返回指向数组首元素的指针
}
int main() {
int* myArray = createArray();
for (int i = 0; i < 10; ++i) {
std::cout << myArray[i] << " ";
}
std::cout << std::endl;
delete[] myArray; // 释放动态分配的内存
return 0;
}
在 createArray 函数中,我们动态地创建了一个整数数组,并返回了指向其首元素的指针。返回之后,调用者负责管理这个内存空间。
5.3 指针与类
5.3.1 指针this的使用
在类的成员函数中, this 指针指向调用该函数的对象。它是一种特殊的指针,使得成员函数可以访问对象的成员。
#include <iostream>
class Counter {
private:
int count;
public:
Counter() : count(0) {}
void increment() {
this->count++; // 使用this指针访问私有成员变量
}
void print() {
std::cout << "Count: " << this->count << std::endl;
}
};
int main() {
Counter c;
c.increment();
c.print(); // Count: 1
return 0;
}
在这个例子中, increment 函数使用 this->count 来访问当前对象的私有成员变量 count ,并对其增加1。
5.3.2 指针与类成员函数
将指针作为类成员函数的参数是一种常见的做法,特别是用于访问和修改类的私有或受保护成员。
#include <iostream>
class MyClass {
private:
int value;
public:
MyClass(int val) : value(val) {}
void setValue(int val) {
value = val;
}
int getValue() {
return value;
}
void printValue(int* ptr) {
*ptr = value; // 修改传入指针指向的值
}
};
int main() {
int val;
MyClass obj(10);
obj.setValue(20);
std::cout << "Object value: " << obj.getValue() << std::endl;
obj.printValue(&val); // 传递val的地址,函数修改val的值
std::cout << "Passed value: " << val << std::endl;
return 0;
}
在这个例子中, printValue 成员函数接收一个指向 int 的指针,并修改指针指向的值。通过这种方式,成员函数可以影响其参数指向的外部数据。
6. 面向对象编程基础
面向对象编程(Object-Oriented Programming, OOP)是C++编程范式的核心,它提供了一种全新的思考和解决问题的方法。面向对象编程关注的是数据(对象)和操作数据的函数(方法)的封装,通过继承和多态性实现代码的重用和扩展。在这一章节中,我们将深入探讨OOP的基础概念,并了解如何在C++中实现类的定义、对象的创建、访问控制和封装、以及构造函数与析构函数。
6.1 类的定义与对象的创建
6.1.1 类的声明与定义
在C++中,类是创建对象的模板或蓝图。一个类定义了具有相同属性和行为的对象集合。类由数据成员(变量)和函数成员(方法)组成,它们共同定义了类的结构和功能。
class MyClass {
public:
void publicFunction() {
// 公共函数的实现
}
private:
int privateVariable; // 私有数据成员
};
在上面的类定义中, MyClass 拥有一个私有成员变量 privateVariable 和一个公共成员函数 publicFunction 。类的定义通常包含在头文件中,并且使用 class 关键字,后面紧跟着类名和一对大括号 {} 包含类的成员。
6.1.2 对象的创建和使用
对象是根据类模板创建的实体。在C++中,对象可以是全局的、局部的、或者动态分配的。对象被创建时,构造函数会自动调用,以初始化对象的状态。
int main() {
MyClass obj; // 创建类的实例
obj.publicFunction(); // 调用对象的公共成员函数
return 0;
}
在上面的示例中, MyClass 的一个实例被创建,并命名为 obj 。然后调用了这个对象的 publicFunction 方法。需要注意的是,对象的私有成员变量和函数只能被类的成员函数、友元函数或友元类访问。
6.2 访问控制与封装
6.2.1 访问说明符public和private
在C++中, public 、 private 和 protected 是三种访问说明符,它们定义了类成员的访问权限。
public成员可以在任何地方被访问。private成员只能被类的成员函数、友元函数和友元类访问。protected成员的访问权限介于public和private之间,通常用于继承的情况。
6.2.2 封装的作用和意义
封装是面向对象编程的一个基本原则,它将数据和操作数据的函数捆绑在一起,并隐藏了内部实现细节。通过封装,可以保护对象的状态不被外部直接修改,从而防止数据的不一致性。
class Account {
private:
double balance; // 私有成员变量,表示余额
public:
void deposit(double amount) {
balance += amount; // 存款方法
}
double getBalance() const {
return balance; // 获取余额的方法
}
};
在上面的 Account 类中, balance 成员变量被声明为 private ,以确保只有 Account 类的成员函数可以修改余额。这样,就实现了封装的基本原则,即通过公共接口 deposit 和 getBalance 方法来控制对 balance 的访问。
6.3 构造函数与析构函数
6.3.1 构造函数的分类和使用
构造函数是一种特殊类型的成员函数,它在创建对象时自动调用,用于初始化对象的状态。构造函数可以分为几种类型:
- 默认构造函数:没有参数的构造函数,用于创建对象的默认实例。
- 参数化构造函数:具有参数的构造函数,允许在创建对象时初始化对象的状态。
- 拷贝构造函数:一个特殊的构造函数,用于创建一个对象作为另一个对象的副本。
class Complex {
public:
double real, imag;
Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {
// 参数化构造函数
}
Complex(const Complex& c) {
// 拷贝构造函数
real = c.real;
imag = c.imag;
}
};
6.3.2 析构函数的作用和时机
析构函数是另一种特殊的成员函数,在对象生命周期结束时自动调用,用于执行清理工作,如释放分配的资源。析构函数总是与构造函数相对应,并且具有相同的名称,但前面有一个波浪号 ~ 。
class File {
public:
File(const char* filename) {
// 打开文件...
}
~File() {
// 关闭文件...
}
};
在上面的例子中, File 类的构造函数负责打开文件,而析构函数则负责在对象销毁时关闭文件。
以上内容为第六章面向对象编程基础的概述。面向对象编程为软件开发提供了巨大的灵活性和强大的功能,其核心概念如类的定义、对象的创建、封装、继承和多态性,共同构成了C++面向对象的编程范式。在下一章节中,我们将继续深入探讨继承和多态性,以及它们在C++中的应用。
7. 类和对象的构造与析构
7.1 深入理解构造函数
7.1.1 默认构造函数与拷贝构造函数
构造函数是类的一种特殊的成员函数,它在创建对象时自动调用,用于初始化对象的成员变量。默认构造函数是在没有任何实参的情况下调用的构造函数。当程序员没有为类显式提供任何构造函数时,编译器会自动提供一个默认构造函数。然而,如果类中定义了其他构造函数,编译器将不会自动生成默认构造函数。
拷贝构造函数是一种特殊的构造函数,它只有一个参数,该参数是对同类对象的引用(通常是const引用)。它的作用是基于一个已存在的同类对象来创建一个新的对象。
class MyClass {
public:
MyClass() {} // 默认构造函数
MyClass(const MyClass& other) {} // 拷贝构造函数
};
7.1.2 构造函数的初始化列表
构造函数可以使用初始化列表来初始化类的数据成员和基类。初始化列表在函数体执行之前进行数据成员的初始化,这比在函数体中赋值更为高效。
class MyClass {
private:
int value;
double dValue;
public:
MyClass(int val, double dval) : value(val), dValue(dval) {} // 使用初始化列表
};
7.2 深入理解析构函数
7.2.1 析构函数的必要性和时机
析构函数是与构造函数相对应的,它在对象生命周期结束时被自动调用,用于进行资源的清理工作,如释放内存、关闭文件等。析构函数可以防止资源泄露,保证对象结束时的完整性。一个类只能有一个析构函数,并且它不能有参数,也不能有返回值。
class MyClass {
public:
~MyClass() { // 析构函数
// 清理资源的代码
}
};
7.2.2 析构函数中的资源释放
在析构函数中释放资源是其重要职责之一。析构函数中的操作应当足够安全,防止在对象销毁时出现异常。通常,在析构函数中处理的资源包括动态分配的内存、文件句柄、网络连接等。
class FileResource {
public:
FileResource(const char* filename) {
file_ = fopen(filename, "r");
}
~FileResource() {
if (file_ != nullptr) {
fclose(file_);
}
}
private:
FILE* file_;
};
以上就是第七章的核心内容,主要涵盖了构造函数和析构函数的深入理解及其应用。接下来的章节我们将探讨继承和多态性的应用,这将为读者展示面向对象编程的另一面。
简介:C++是一种高效的编程语言,适用于多种软件开发领域。本书详细阐述了C++的基础语法、函数、内存管理、指针、面向对象编程、模板、标准库和异常处理等核心概念,并通过丰富的示例和练习,帮助初学者建立坚实的基础。学习C++不仅能够掌握一种强大的编程工具,还将理解计算机科学的核心原理,为IT领域的职业生涯打下坚实基础。
更多推荐


所有评论(0)