Skip to content

C++ 基础语法

声明与定义

  • 声明:只是说明名字的存在,外部声明使用extern关键字,函数可以不使用,因为不带函数体的函数名连同参数表或返回值,自动地作为一个声明。
  • 定义:为名字分配内存,它可以同时是声明。

  • 初始化:在变量被声明时给变量赋值,称变量被初始化。声明时未赋值称变量未初始化:int a=0;初始化,而int a;未初始化。

  • C++中函数声明时可以仅给定参数类型而不给参数名称,在函数定义时再给定具体名称,在C中不行。

void func(int);
void func(int x){x++;}

void*

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

union

联合体(union):联合体中的所有成员共享同一块内存空间,因此其内存大小是所有成员中最大成员的大小。

enum

C中enum用于定义一组具有关联的整型常量。默认情况下,枚举常量会从0开始逐一递增,可以显式指定枚举值大小。

enum EnumName {
    CONSTANT1,  // 默认为 0
    CONSTANT2,  // 默认为 1
    CONSTANT3,  // 默认为 2
    constant4 = 5,
    constant5   //默认为6
};

C++中除了上述用法还引入了强类型枚举(enum class),枚举常量不再自动暴露在作用域中,必须通过EnumName::CONSTANT来访问。

enum class EnumName {
    CONSTANT1,
    CONSTANT2,
    CONSTANT3
};
cout << EnumName::CONSTANT3;

static

static修饰的静态全局或局部变量在声明时未赋值时,都会被自动初始化为0,同C语言。

相关内存段:

  • .data段:包含已初始化的全局变量和静态变量(包括非零的初始化值)
  • .bss段:包含未初始化的全局变量和静态变量,或者初始化为零的变量。

字符串

  • " " 代表字符串字面量(const char[]),存储于rodata段
  • ' ' 代表字符字面量(char)
char a = "A"; //错误 "A" 表示字符串,为 char[2]
char* b = 'B';//错误 'B' 是个字符,为char
char* c = "abcd"; //会报warning,因为没有const
const d = "bdcd";// 正确

字符串字面量

  • C++中强制要求 char* 必须指向可修改的内存位置,而字符串字面量(形如"ssss")存储在rodata段,故上述第三行会warning。

  • C 语言中没有强制要求 char* 必须指向可修改的内存位置。故上述第三行不报warning。


常量相关

const

用于声明常量或常量表达式,表示数据不可修改,必须进行初始化,即声明时赋值。

  • 编译时常量:存储在只读数据段(.rodata 或 .rdata),程序加载时由操作系统直接映射到内存,不可修改(修改会导致段错误)
  • const int x=1;
  • 运行时常量:运行时才能确定,编译器无法确定值,
作用域 存储位置 示例
全局/静态 数据段 const int rt_global=get_val();
局部(函数内) const int local=cal();
动态分配 const int* p=new const int(42);
const int x;

一般来说const int x;的写法是错误的,因为声明时未初始化,不过在类中定义成员变量这是可以的,前提是:常量成员必须在构造函数中使用初始化列表进行初始化,且只能使用初始化列表初始化。例:

    class ConstExample {
    public:
        ConstExample(int val) : constValue(val) {} // 正确
        // ConstExample(int val) { constValue = val; } // 错误!const 成员不能在构造函数体内赋值
    private:
        const int constValue;
    };

使用方式

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

头文件

  • #include<>引用的是编译器标准库里的头文件
  • #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中声明了两次。

作用域

命名空间(Namespace)

C++中的命名空间是一种用于组织代码、避免命名冲突的机制。

1. 命名空间的作用

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

2. 定义命名空间

namespace MySpace {
    void func(); // 函数声明
    class MyClass { /*...*/ }; // 类定义
    int x; // 变量
}
  • 命名空间可以嵌套

    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):成员直接可见于外层命名空间,常用于版本控制:

    namespace Lib {
        inline namespace v1 { void func(); }
    }
    Lib::func(); // 实际调用v1中的func
    

8. 实践

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

示例代码

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;
    }
}

inline内联函数

在C++中,inline 是一个重要的关键字,主要用于优化代码执行效率和解决某些编译链接问题。

1. 内联函数的作用

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

2. 基本语法

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

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

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

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

4. 内联函数 vs 宏

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

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

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

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

6. 注意事项

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

7. 最佳实践

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

8. 示例代码

正确用法

// 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);
}

所有权(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