Skip to content

C++ 基础语法

声明与定义

声明:只是向计算机介绍名字。使用extern关键字,但是如果是函数可以不使用,不带函数体的函数名连同参数表或返回值,自动地作为一个 声明。 定义:为名字分配内存,它可以同时是声明。

头文件

#include的头文件如果用标准头文件或标准库一般用<>,而自定义头文件一般用"",便于区分。

C++标准库头文件不加.h,而C标准库头文件需要加,如果在 C++ 中使用,通常使用 c 前缀并去掉 .h 后缀,如#include<cstdio>

防止头文件多次声明导致的错误:

// myheader.h
#ifndef MYHEADER_H   // 使用头文件名作为前缀
#define MYHEADER_H
// 头文件的实际内容
#endif

//C++中也可以在头文件中使用如下简化的预处理指令
#pragma once

头文件多次声明的例子:main.cpp中include A.hB.h,而B.h中也include了A.h,那么A.h就在main.cpp中声明了两次。

作用域

命名空间

C++中的命名空间(Namespace)是一种用于组织代码、避免命名冲突的机制。以下是对命名空间的详细总结:

1. 命名空间的作用

  • 避免命名冲突:将标识符(变量、函数、类等)封装在独立的命名空间中,防止不同库或模块中的同名标识符冲突。
  • 组织代码结构:将相关代码分组,提高可读性和可维护性。

2. 定义命名空间

namespace MySpace {
    void func(); // 函数声明
    class MyClass { /*...*/ }; // 类定义
    int x; // 变量
}
  • 命名空间可以嵌套(C++17支持简洁写法):

    cpp namespace A::B { // C++17及以后 void nestedFunc(); } // 传统写法 namespace A { namespace B { void nestedFunc(); } }

3. 使用命名空间

  • 作用域解析运算符 ::

    MySpace::func(); // 调用MySpace中的函数
    A::B::nestedFunc(); // 调用嵌套命名空间中的函数
    
  • using 声明(引入单个名称):

    using MySpace::func;
    func(); // 直接调用
    
  • using 指令(引入整个命名空间,慎用):

    using namespace MySpace;
    func(); // 直接调用
    
  • 注意:避免在头文件中使用 using namespace,可能导致命名污染。

4. 标准命名空间 std

  • 标准库内容位于 std 命名空间:

    std::cout << "Hello"; // 显式使用
    
  • 推荐使用作用域解析或 using 声明,而非全局引入:

    using std::cout; // 仅引入cout
    

5. 匿名命名空间

  • 用于限制标识符仅在当前文件可见(内部链接):

    namespace { // 匿名命名空间
        void internalFunc() { /*...*/ }
    }
    
  • 等效于C的 static,但更符合C++风格。

6. 命名空间别名

  • 简化长命名空间名称:
namespace AB = A::B; // 别名
AB::nestedFunc(); // 等价于A::B::nestedFunc()

7. 其他特性

  • 重新打开命名空间:可在不同位置扩展命名空间内容。
namespace MySpace {
    void anotherFunc(); // 添加新函数
}
  • 全局命名空间:通过 :: 访问全局作用域:
    ::globalFunc(); // 调用全局函数
    ```
- **内联命名空间**C++11):成员直接可见于外层命名空间常用于版本控制
    ```cpp
    namespace Lib {
        inline namespace v1 { void func(); }
    }
    Lib::func(); // 实际调用v1中的func

8. 最佳实践

  • 避免污染全局作用域:尽量使用作用域解析或 using 声明。
  • 合理组织代码:按模块或库划分命名空间。
  • 头文件规范:在头文件中声明命名空间,避免 using 指令。

示例代码

#include <iostream>

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

// 使用别名
namespace MC = Math::Calc;

int main() {
    std::cout << MC::add(3, 4) << std::endl; // 输出7
    return 0;
}

范围分解

使用范围分解符/作用域解析符 :: 可以指定使用哪个范围(全局、类内、结构体内)的变量和函数。

全局范围::函数/变量名

int a;
void f(){}
struct S{
    int a;
    void f(){}
}
void S::f(){
    ::a++; //the gobal a
    ::f(); //the gobal f,没有::则会递归定义
}

缺省参数

缺省参数是在函数声明时就已给定的一个值,如果我们在调用函数时没有指定这一参数的值,编译器就会自动地插上这个值。

注意:

  • 只有参数列表的后部参数才可是缺省的,也就是说,我们不可以在一个缺省参数后面又跟一个非缺省的参数。
  • 一旦我们开始使用缺省参数,那么这个参数后面的所有参数都必须是缺省的。(这可以从第一条中导出。)
  • 默认参数的值可以在函数声明中指定,也可以在函数定义中指定,但不能在声明和定义中都给出默认值,否则就会报默认参数重复定义的错误。
// 函数声明:在声明时为参数指定默认值
void greet(string name = "Guest", int times = 1);
int main()...
// 函数定义:不需要再次指定默认值
void greet(string name/*="Guest"*/, int times/*=1*/) {
    for (int i = 0; i < times; ++i) {
        cout << "Hello, " << name << "!" << endl;
    }
}

这和python类似。

void*

C++允许将任何类型的指针赋给 void *(这是 void* 的最初的意图,它要求 void* 足够大,以存放任何类型的指针),但不允许将void* 指针赋给任何其他类型的指针。因为C++是类型严格的。在C中两者都可以。

Reference

reference(引用)本质是个变量的别名,引用允许我们通过别名访问变量,而不需要复制其值。type &ref = variable。 特点:

  • 引用在声明时就必须初始化且之后不能改变引用的对象。
  • 引用本质上是一个别名,它和原始对象共享同一内存地址。
  • 引用用于函数参数传递时,避免了大对象的复制,并且可以在函数内部修改外部对象的值。

用法:

void increment(int &x) {
    x++;  // 通过引用修改外部变量的值
}


int& getMax(int &a, int &b) {
    return (a > b) ? a : b;  // 返回引用
}
int x=10,y=20;
getMax(x,y)=30;  //现在y=30


void printValue(const int &x) {   //传递大型对象/结构体时尤其重要,可以避免不必要的复制,同时保护原始数据。
    std::cout << x << std::endl;  // 不能修改 x
}
int x;
printValue(x);

与指针的区别

特性 引用(Reference) 指针(Pointer)
语法 type &ref = variable; type* ptr = &variable;
是否可为空 不可为空(必须引用有效的对象) 可以为空(指向 nullptr
是否可以修改指向对象 不可以修改引用指向的对象 可以通过修改指针来改变它指向的对象
内存开销 引用没有额外的内存开销,它是一个别名 指针有额外的内存开销(存储指针本身的地址)
使用场景 更常用于函数参数传递、返回引用等 更灵活,可以指向不同的对象,但需要管理内存
常量引用 const type & const type*

限制:

  • No pointers to reference: int& * p;//illegal;
  • No array of references

const

C++中关键字,用于声明常量或常量表达式,表示数据不可修改。

  • 编译时常量:编译时就被确定下来,编译器可以直接替换常量值,不会存储在运行时内存
  • const int x=1;
  • 运行时常量:运行时才能确定,编译器无法确定值:内存中储存其值
  • const int x;

使用方式

const 可在类型前也可在类型后:const int x;int const x相同。前者更为常见。

constexpr

constexpr 声明的常量要求值在编译时可确定

pointers and const

char* const p='123'; //p is a const pointer
p++;//error
*p='abc'//right

const char* q='abc'; //q* is a const char
*q='1';//error
q++; //right

char const* r = &p;//r is a const char

字符串

  • "" 代表字符串字面量(const char[])
  • '' 代表字符字面量(char)
char a = "A"; //错误 "A" 表示字符串,为 char[2]
char* b = 'B';//错误 'B' 是个字符,为char

inline内联函数

在C++中,inline 是一个重要的关键字,主要用于优化代码执行效率和解决某些编译链接问题。以下是关于内联函数的详细总结:


1. 内联函数的作用

  • 减少函数调用开销:将函数体直接插入调用处,避免压栈、跳转等开销。
  • 避免宏的缺陷:提供类型安全、可调试的替代方案(相比C语言的宏)。
  • 解决头文件中的函数定义问题:允许多个编译单元包含同一函数定义而不会引发链接错误(需结合头文件使用)。

2. 基本语法

inline int add(int a, int b) {
    return a + b;
}
  • inline 关键字放在函数定义前(声明中不需要)。
  • 类内直接定义的成员函数隐式内联

    class Calculator {
    public:
        int add(int a, int b) { // 自动为内联函数
            return a + b;
        }
    };
    

3. 编译器如何处理内联?

  • 建议而非强制inline 只是对编译器的优化建议,编译器可能拒绝内联(如递归函数、复杂函数)。
  • 优化权衡:编译器会根据函数体大小、调用频率等因素决定是否内联。
  • 跨编译单元可见性:内联函数需在每个使用它的编译单元中可见(通常定义在头文件中)。

4. 适用场景

  1. 短小且频繁调用的函数(如简单数学运算、访问器方法)。
  2. 头文件中的工具函数:需在多个源文件中复用。
  3. 替代宏函数:避免宏的副作用(如参数重复求值)。

5. 注意事项与陷阱

  • 代码膨胀:过度内联会增加二进制文件体积,可能降低缓存命中率。
  • 调试困难:内联后的函数在调用栈中可能不可见。
  • 虚函数不能内联:虚函数调用在运行时确定,无法静态展开。
  • 递归函数通常无法内联(除非编译器进行尾递归优化)。

6. 内联函数 vs 宏

特性 内联函数
类型安全 ✔️ ❌(文本替换)
调试支持 ✔️ ❌(预处理器阶段展开)
作用域 遵循C++作用域规则 无作用域概念
参数求值 参数只求值一次 可能多次求值(如MAX(x++)

7. C++17扩展:内联变量

C++17允许在头文件中定义inline变量,解决静态成员变量初始化问题:

// 头文件 MyClass.h
class MyClass {
public:
    inline static int count = 0; // 直接初始化
};

8. 最佳实践

  1. 优先让编译器决定:现代编译器能自动内联简单函数。
  2. 仅显式标记关键函数:如性能敏感的短函数。
  3. 头文件中定义内联函数:确保跨编译单元可见性。
  4. 避免复杂逻辑:循环、递归、大函数不适合内联。
  5. 结合constexpr使用(C++11+):编译时计算的函数更高效。

9. 示例代码

正确用法

// utils.h
inline int square(int x) { // 在头文件中定义
    return x * x;
}

// main.cpp
#include "utils.h"
int main() {
    int a = square(5); // 可能被替换为 a = 5*5;
    return 0;
}

错误用法

// 错误:递归函数无法内联
inline int factorial(int n) {
    return (n <= 1) ? 1 : n * factorial(n-1);
}

10. 底层原理浅析

当函数被内联时,编译器会执行以下操作: 1. 消除调用指令:直接展开函数体到调用位置。 2. 优化上下文:能进行更激进的优化(如常量传播)。 3. 调整符号链接:避免多个定义冲突(需遵守ODR规则)。


合理使用inline可以在不影响代码可读性的前提下提升性能,但需结合性能分析工具验证实际效果。

所有权(ownership)

在编程中,所有权(Ownership) 是一个核心概念,尤其在系统级语言(如 C++/Rust)中至关重要。它定义了某个对象或资源的归属关系,明确谁负责资源的生命周期管理(如内存分配、释放、文件句柄关闭等)。

所有权的核心规则

  1. 唯一性:同一时刻,资源只能有一个明确的拥有者。
  2. 责任绑定:拥有者负责资源的创建和销毁。
  3. 转移控制:所有权可以通过特定操作(如移动语义)转移给其他实体。

C++ 中的所有权体现

1. 原始指针(无明确所有权)

int* p = new int(42);  // 手动分配内存
// 必须手动释放,否则内存泄漏
delete p;
  • 问题:所有权不明确,容易导致泄漏或重复释放。

2. 引用

所有权是资源的归属权,引用被设计为一种临时、轻量级的资源访问机制,而非资源的管理者,它不会管理资源的生命周期。

3. 智能指针(显式所有权管理)

  • std::unique_ptr(独占所有权)

    auto p = std::make_unique<int>(42); // 唯一拥有资源
    auto q = std::move(p);              // 所有权转移,p 变为空
    
  • 所有权唯一,不可复制,只能移动。

  • std::shared_ptr(共享所有权)

    auto p = std::make_shared<int>(42);
    auto q = p;  // 共享所有权,引用计数+1
    
  • 通过引用计数管理生命周期,所有权被多个对象共享。

所有权的核心作用

场景 所有权的意义
内存安全 避免内存泄漏(忘记释放)或野指针(访问已释放内存)。
资源管理 统一管理文件句柄、网络连接、锁等资源,确保及时释放。
并发安全 明确所有权可防止多个线程同时修改同一资源,降低数据竞争风险。
代码可维护性 通过类型系统(如 unique_ptr)强制表达所有权意图,减少隐性错误。

所有权转移示例(C++)

void takeOwnership(std::unique_ptr<int> ptr) {
    // 现在 ptr 拥有资源
} // 函数结束,ptr 销毁,资源自动释放

int main() {
    auto p = std::make_unique<int>(42);
    takeOwnership(std::move(p)); // 转移所有权给函数参数
    // 此时 p 已为空,无法再访问资源
    return 0;
}

所有权的设计哲学

  1. RAII(资源获取即初始化)
  2. 资源生命周期绑定到对象作用域(如 unique_ptr 在析构时释放内存)。
  3. 最小权限原则
  4. 只让必要的代码拥有资源所有权,其他代码通过引用访问。
  5. 显式优于隐式
  6. 用类型系统(如智能指针)明确表达所有权,而非依赖约定。

Comments