MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++ 语法深入解析与实践指南

2023-04-147.7k 阅读

1. C++ 基础语法要素

1.1 变量与数据类型

在 C++ 中,变量是存储数据的容器,而数据类型则决定了变量能够存储的数据种类和占用的内存空间大小。

基本数据类型:C++ 提供了丰富的基本数据类型,包括整型(int)、浮点型(floatdouble)、字符型(char)、布尔型(bool)等。例如,int 类型用于存储整数,在大多数系统中占用 4 个字节。以下是一个简单的变量声明和初始化示例:

int num = 10;
float pi = 3.14f;
char ch = 'A';
bool isTrue = true;

这里 num 是一个 int 类型变量,初始值为 10;pifloat 类型变量,注意 float 字面量需要加上 f 后缀;chchar 类型变量,存储字符 'A'isTruebool 类型变量,值为 true

自定义数据类型:除了基本数据类型,C++ 允许用户自定义数据类型,如结构体(struct)、联合体(union)和类(class)。结构体可以将不同类型的数据组合在一起,例如:

struct Point {
    int x;
    int y;
};
Point p = {1, 2};

这里定义了一个 Point 结构体,包含两个 int 类型成员 xy,然后创建了一个 Point 类型变量 p 并初始化。

1.2 运算符

C++ 拥有丰富的运算符,包括算术运算符、赋值运算符、比较运算符、逻辑运算符、位运算符等。

算术运算符:常见的算术运算符有 +(加)、-(减)、*(乘)、/(除)和 %(取模)。例如:

int a = 5, b = 3;
int sum = a + b;
int product = a * b;
int remainder = a % b;

这里 sum 的值为 8,product 的值为 15,remainder 的值为 2。

赋值运算符= 是最基本的赋值运算符,用于将右侧的值赋给左侧的变量。复合赋值运算符如 +=-=*=/= 等则是一种便捷写法。例如:

int num = 5;
num += 3; // 等价于 num = num + 3;

执行后 num 的值变为 8。

比较运算符:用于比较两个值的大小关系,返回布尔值。常见的比较运算符有 ==(等于)、!=(不等于)、>(大于)、<(小于)、>=(大于等于)和 <=(小于等于)。例如:

int a = 5, b = 3;
bool isGreater = a > b; // true
bool isEqual = a == b; // false

逻辑运算符:逻辑运算符用于组合布尔值,包括 &&(逻辑与)、||(逻辑或)和 !(逻辑非)。例如:

bool a = true, b = false;
bool result1 = a && b; // false
bool result2 = a || b; // true
bool result3 =!a; // false

位运算符:位运算符对整数的二进制位进行操作,包括 &(按位与)、|(按位或)、^(按位异或)、~(按位取反)、<<(左移)和 >>(右移)。例如:

int a = 5; // 二进制 00000101
int b = 3; // 二进制 00000011
int andResult = a & b; // 二进制 00000001,值为 1
int orResult = a | b; // 二进制 00000111,值为 7
int xorResult = a ^ b; // 二进制 00000110,值为 6
int leftShiftResult = a << 1; // 二进制 00001010,值为 10

2. 控制结构

2.1 顺序结构

顺序结构是程序中最基本的执行结构,语句按照书写顺序依次执行。例如:

int a = 5;
int b = 3;
int sum = a + b;
std::cout << "Sum is: " << sum << std::endl;

在这个例子中,先声明并初始化 ab,然后计算它们的和并存储在 sum 中,最后输出结果。

2.2 选择结构

if - else 语句:用于根据条件决定执行不同的代码块。例如:

int num = 10;
if (num > 5) {
    std::cout << "The number is greater than 5" << std::endl;
} else {
    std::cout << "The number is less than or equal to 5" << std::endl;
}

这里根据 num 是否大于 5 来决定输出不同的信息。

switch - case 语句:用于多分支选择,根据一个整型或枚举类型的表达式的值来选择执行不同的分支。例如:

int day = 3;
switch (day) {
    case 1:
        std::cout << "Monday" << std::endl;
        break;
    case 2:
        std::cout << "Tuesday" << std::endl;
        break;
    case 3:
        std::cout << "Wednesday" << std::endl;
        break;
    default:
        std::cout << "Invalid day" << std::endl;
        break;
}

switch 语句根据 day 的值选择对应的 case 分支执行,如果没有匹配的 case,则执行 default 分支。注意每个 case 分支末尾通常需要使用 break 语句跳出 switch 结构,否则会继续执行下一个 case 分支。

2.3 循环结构

for 循环:适用于已知循环次数的情况。其基本语法为 for (初始化表达式; 条件表达式; 调整表达式)。例如:

for (int i = 0; i < 5; i++) {
    std::cout << "Iteration " << i << std::endl;
}

这里 i 从 0 开始,每次循环增加 1,当 i 小于 5 时执行循环体,输出当前的迭代次数。

while 循环:在条件满足时重复执行循环体,直到条件不满足为止。例如:

int num = 0;
while (num < 5) {
    std::cout << "Number is: " << num << std::endl;
    num++;
}

在这个例子中,num 初始值为 0,只要 num 小于 5,就会执行循环体并输出 num 的值,然后 num 自增 1。

do - while 循环:与 while 循环类似,但它先执行一次循环体,然后再判断条件。例如:

int num = 0;
do {
    std::cout << "Number is: " << num << std::endl;
    num++;
} while (num < 5);

这里无论条件是否满足,循环体至少会执行一次。

3. 函数

3.1 函数的定义与声明

函数是一段完成特定任务的代码块。函数定义包括函数头和函数体,函数头包含函数返回类型、函数名和参数列表,函数体是实现具体功能的代码。例如:

int add(int a, int b) {
    return a + b;
}

这里定义了一个名为 add 的函数,它接受两个 int 类型参数 ab,返回它们的和。

函数声明则是告诉编译器函数的存在及其参数和返回类型,函数声明通常放在头文件中。例如:

int add(int a, int b);

声明和定义的区别在于声明不包含函数体,只是提供函数的基本信息。

3.2 函数参数与返回值

函数参数:函数可以接受零个或多个参数,参数在函数调用时传递实际值。参数分为值传递、指针传递和引用传递。

  • 值传递:函数接收的是实参的副本,对形参的修改不会影响实参。例如:
void increment(int num) {
    num++;
}
int main() {
    int value = 5;
    increment(value);
    std::cout << "Value is still: " << value << std::endl; // 输出 5
    return 0;
}
  • 指针传递:通过传递指针,函数可以直接访问和修改实参的值。例如:
void increment(int* num) {
    (*num)++;
}
int main() {
    int value = 5;
    increment(&value);
    std::cout << "Value is now: " << value << std::endl; // 输出 6
    return 0;
}
  • 引用传递:引用是对象的别名,与指针传递类似,但语法更简洁。例如:
void increment(int& num) {
    num++;
}
int main() {
    int value = 5;
    increment(value);
    std::cout << "Value is now: " << value << std::endl; // 输出 6
    return 0;
}

返回值:函数可以返回一个值,返回值的类型在函数定义的返回类型中指定。如果函数不需要返回值,可以使用 void 作为返回类型。例如:

void printMessage() {
    std::cout << "This is a message" << std::endl;
}

3.3 函数重载

函数重载是指在同一作用域内,可以定义多个同名函数,但它们的参数列表不同(参数个数、类型或顺序不同)。例如:

int add(int a, int b) {
    return a + b;
}
double add(double a, double b) {
    return a + b;
}
int add(int a, int b, int c) {
    return a + b + c;
}

在调用 add 函数时,编译器会根据传递的参数类型和个数来选择合适的函数版本。

3.4 递归函数

递归函数是指在函数内部调用自身的函数。递归函数需要有终止条件,否则会导致无限递归。例如,计算阶乘的递归函数:

int factorial(int n) {
    if (n == 0 || n == 1) {
        return 1;
    } else {
        return n * factorial(n - 1);
    }
}

这里 factorial 函数在 n 为 0 或 1 时返回 1,否则通过递归调用 factorial(n - 1) 来计算 n 的阶乘。

4. 数组与字符串

4.1 数组

数组是一种数据结构,用于存储多个相同类型的数据元素。

一维数组:其声明形式为 类型 数组名[元素个数]。例如:

int numbers[5] = {1, 2, 3, 4, 5};

这里定义了一个 int 类型的一维数组 numbers,包含 5 个元素。数组元素通过下标访问,下标从 0 开始。例如,numbers[0] 的值为 1,numbers[4] 的值为 5。

二维数组:可以看作是数组的数组,常用于表示矩阵等数据结构。其声明形式为 类型 数组名[行数][列数]。例如:

int matrix[3][2] = {
    {1, 2},
    {3, 4},
    {5, 6}
};

这里定义了一个 3 行 2 列的二维数组 matrix。访问二维数组元素时需要使用两个下标,如 matrix[1][0] 的值为 3。

4.2 字符串

在 C++ 中,字符串可以用字符数组或 std::string 类来表示。

字符数组表示字符串:以空字符 '\0' 作为字符串的结束标志。例如:

char str1[] = "Hello";

这里 str1 是一个字符数组,存储了字符串 "Hello",实际占用 6 个字节,因为字符串末尾自动添加了 '\0'

std::stringstd::string 类提供了更方便的字符串操作方法。例如:

#include <string>
#include <iostream>
int main() {
    std::string str2 = "World";
    std::string combined = "Hello, " + str2;
    std::cout << combined << std::endl;
    return 0;
}

std::string 类支持字符串拼接、比较、查找等丰富的操作。例如,combined 通过拼接 "Hello, "str2 得到 "Hello, World"

5. 内存管理

5.1 栈与堆内存

在 C++ 中,程序使用的内存分为栈和堆。

栈内存:栈内存用于存储局部变量、函数参数等。栈内存的分配和释放由编译器自动管理,速度较快。例如:

void function() {
    int num = 5;
}

这里 num 存储在栈内存中,当 function 函数结束时,num 占用的栈内存会被自动释放。

堆内存:堆内存用于动态分配内存,通过 newdelete 运算符(或 mallocfree 函数,C 风格)来管理。例如:

int* ptr = new int;
*ptr = 10;
delete ptr;

这里通过 new 运算符在堆内存中分配了一个 int 类型的空间,并将其地址赋给 ptr,然后为该空间赋值 10,最后使用 delete 运算符释放该堆内存空间。

5.2 动态内存分配

newdelete 运算符new 用于在堆内存中分配内存并返回指向该内存的指针,delete 用于释放 new 分配的内存。例如:

int* arr = new int[5];
for (int i = 0; i < 5; i++) {
    arr[i] = i * 2;
}
delete[] arr;

这里通过 new int[5] 在堆内存中分配了一个包含 5 个 int 类型元素的数组,并使用 delete[] 释放该数组内存。注意对于数组,要使用 delete[] 来释放,否则可能导致内存泄漏。

智能指针:为了更安全地管理动态内存,C++ 引入了智能指针。std::unique_ptrstd::shared_ptrstd::weak_ptr 是 C++ 标准库提供的智能指针类型。例如,使用 std::unique_ptr

#include <memory>
std::unique_ptr<int> ptr(new int(10));

std::unique_ptr 拥有对所指向对象的唯一所有权,当 std::unique_ptr 对象销毁时,会自动释放其所指向的内存。std::shared_ptr 允许多个指针共享对同一个对象的所有权,通过引用计数来管理对象的生命周期。std::weak_ptr 则是一种弱引用,不增加对象的引用计数,主要用于解决循环引用问题。

6. 面向对象编程(OOP)

6.1 类与对象

类的定义:类是一种自定义数据类型,它封装了数据(成员变量)和操作这些数据的函数(成员函数)。例如:

class Circle {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    double getArea() {
        return 3.14 * radius * radius;
    }
};

这里定义了一个 Circle 类,包含一个私有成员变量 radius(表示圆的半径)和两个公有成员函数:构造函数 Circle(double r) 用于初始化 radiusgetArea 函数用于计算圆的面积。

对象的创建与使用:类的对象是类的实例化。例如:

Circle c(5.0);
double area = c.getArea();

这里创建了一个 Circle 类的对象 c,半径为 5.0,然后调用 getArea 函数计算并获取圆的面积。

6.2 封装

封装是将数据和操作数据的方法组合在一起,并对外部隐藏数据的细节。在 Circle 类中,radius 被声明为私有成员变量,外部代码无法直接访问,只能通过公有成员函数 getArea 间接访问和操作与 radius 相关的数据,这就是封装的体现。

6.3 继承

继承允许一个类(子类)从另一个类(父类)获取属性和行为。例如:

class Shape {
public:
    virtual double getArea() {
        return 0;
    }
};
class Rectangle : public Shape {
private:
    double width;
    double height;
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    double getArea() override {
        return width * height;
    }
};

这里 Rectangle 类继承自 Shape 类,使用 public 关键字表示继承方式。Rectangle 类继承了 Shape 类的 getArea 函数,并通过 override 关键字重写了该函数以实现自己的面积计算逻辑。

6.4 多态

多态是指不同的对象对同一消息作出不同的响应。C++ 中的多态通过虚函数和指针或引用实现。例如:

Shape* shapes[2];
shapes[0] = new Rectangle(3, 4);
shapes[1] = new Circle(5);
for (int i = 0; i < 2; i++) {
    std::cout << "Area is: " << shapes[i]->getArea() << std::endl;
}

这里创建了一个 Shape 指针数组 shapes,分别指向 RectangleCircle 对象。通过基类指针调用虚函数 getArea 时,会根据对象的实际类型(运行时类型)来决定调用哪个类的 getArea 函数,从而实现多态。

7. 模板

7.1 函数模板

函数模板允许定义一种通用的函数,它可以处理不同类型的数据。例如:

template <typename T>
T maximum(T a, T b) {
    return (a > b)? a : b;
}

这里定义了一个函数模板 maximumtemplate <typename T> 表示 T 是一个类型参数。可以使用不同的数据类型调用这个函数模板:

int intMax = maximum(5, 3);
double doubleMax = maximum(3.14, 2.71);

编译器会根据调用时的参数类型自动实例化相应的函数版本。

7.2 类模板

类模板用于创建通用的类,以处理不同类型的数据。例如:

template <typename T>
class Stack {
private:
    T data[100];
    int top;
public:
    Stack() : top(-1) {}
    void push(T value) {
        data[++top] = value;
    }
    T pop() {
        return data[top--];
    }
};

这里定义了一个 Stack 类模板,用于实现一个栈数据结构。可以创建不同类型的栈对象:

Stack<int> intStack;
intStack.push(5);
int num = intStack.pop();

编译器会根据实例化时的类型参数生成相应的类定义。

8. 异常处理

8.1 异常的抛出与捕获

在 C++ 中,异常用于处理程序运行时出现的错误情况。throw 关键字用于抛出异常,try - catch 块用于捕获并处理异常。例如:

#include <iostream>
void divide(int a, int b) {
    if (b == 0) {
        throw "Division by zero";
    }
    std::cout << "Result is: " << a / b << std::endl;
}
int main() {
    try {
        divide(10, 2);
        divide(5, 0);
    } catch (const char* msg) {
        std::cerr << "Error: " << msg << std::endl;
    }
    return 0;
}

divide 函数中,如果 b 为 0,则抛出一个字符串类型的异常。在 main 函数的 try 块中调用 divide 函数,catch 块捕获并处理异常,输出错误信息。

8.2 自定义异常类

除了使用内置类型作为异常,还可以自定义异常类。例如:

class DivideByZeroException : public std::exception {
public:
    const char* what() const noexcept override {
        return "Division by zero";
    }
};
void divide(int a, int b) {
    if (b == 0) {
        throw DivideByZeroException();
    }
    std::cout << "Result is: " << a / b << std::endl;
}
int main() {
    try {
        divide(10, 2);
        divide(5, 0);
    } catch (const DivideByZeroException& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    }
    return 0;
}

这里定义了一个 DivideByZeroException 类,继承自 std::exception 类,并重写了 what 函数以返回错误信息。在 divide 函数中抛出 DivideByZeroException 异常,在 main 函数中捕获并处理该异常。

通过以上对 C++ 语法各个方面的深入解析和实践示例,希望能帮助读者更全面、深入地掌握 C++ 这门强大的编程语言,为进一步的软件开发和编程实践打下坚实的基础。在实际应用中,还需要不断练习和探索,结合具体的应用场景和需求,充分发挥 C++ 的优势。