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.h
和B.h
,而B.h
中也include了A.h
,那么A.h
就在main.cpp
中声明了两次。
作用域
命名空间
C++中的命名空间(Namespace)是一种用于组织代码、避免命名冲突的机制。以下是对命名空间的详细总结:
1. 命名空间的作用
- 避免命名冲突:将标识符(变量、函数、类等)封装在独立的命名空间中,防止不同库或模块中的同名标识符冲突。
- 组织代码结构:将相关代码分组,提高可读性和可维护性。
2. 定义命名空间
-
命名空间可以嵌套(C++17支持简洁写法):
cpp namespace A::B { // C++17及以后 void nestedFunc(); } // 传统写法 namespace A { namespace B { void nestedFunc(); } }
3. 使用命名空间
-
作用域解析运算符
::
: -
using
声明(引入单个名称): -
using
指令(引入整个命名空间,慎用): -
注意:避免在头文件中使用
using namespace
,可能导致命名污染。
4. 标准命名空间 std
-
标准库内容位于
std
命名空间: -
推荐使用作用域解析或
using
声明,而非全局引入:
5. 匿名命名空间
-
用于限制标识符仅在当前文件可见(内部链接):
-
等效于C的
static
,但更符合C++风格。
6. 命名空间别名
- 简化长命名空间名称:
7. 其他特性
- 重新打开命名空间:可在不同位置扩展命名空间内容。
- 全局命名空间:通过
::
访问全局作用域:
::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)
inline内联函数
在C++中,inline
是一个重要的关键字,主要用于优化代码执行效率和解决某些编译链接问题。以下是关于内联函数的详细总结:
1. 内联函数的作用
- 减少函数调用开销:将函数体直接插入调用处,避免压栈、跳转等开销。
- 避免宏的缺陷:提供类型安全、可调试的替代方案(相比C语言的宏)。
- 解决头文件中的函数定义问题:允许多个编译单元包含同一函数定义而不会引发链接错误(需结合头文件使用)。
2. 基本语法
inline
关键字放在函数定义前(声明中不需要)。-
类内直接定义的成员函数隐式内联:
3. 编译器如何处理内联?
- 建议而非强制:
inline
只是对编译器的优化建议,编译器可能拒绝内联(如递归函数、复杂函数)。 - 优化权衡:编译器会根据函数体大小、调用频率等因素决定是否内联。
- 跨编译单元可见性:内联函数需在每个使用它的编译单元中可见(通常定义在头文件中)。
4. 适用场景
- 短小且频繁调用的函数(如简单数学运算、访问器方法)。
- 头文件中的工具函数:需在多个源文件中复用。
- 替代宏函数:避免宏的副作用(如参数重复求值)。
5. 注意事项与陷阱
- 代码膨胀:过度内联会增加二进制文件体积,可能降低缓存命中率。
- 调试困难:内联后的函数在调用栈中可能不可见。
- 虚函数不能内联:虚函数调用在运行时确定,无法静态展开。
- 递归函数通常无法内联(除非编译器进行尾递归优化)。
6. 内联函数 vs 宏
特性 | 内联函数 | 宏 |
---|---|---|
类型安全 | ✔️ | ❌(文本替换) |
调试支持 | ✔️ | ❌(预处理器阶段展开) |
作用域 | 遵循C++作用域规则 | 无作用域概念 |
参数求值 | 参数只求值一次 | 可能多次求值(如MAX(x++) ) |
7. C++17扩展:内联变量
C++17允许在头文件中定义inline
变量,解决静态成员变量初始化问题:
8. 最佳实践
- 优先让编译器决定:现代编译器能自动内联简单函数。
- 仅显式标记关键函数:如性能敏感的短函数。
- 头文件中定义内联函数:确保跨编译单元可见性。
- 避免复杂逻辑:循环、递归、大函数不适合内联。
- 结合
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;
}
错误用法
10. 底层原理浅析
当函数被内联时,编译器会执行以下操作: 1. 消除调用指令:直接展开函数体到调用位置。 2. 优化上下文:能进行更激进的优化(如常量传播)。 3. 调整符号链接:避免多个定义冲突(需遵守ODR规则)。
合理使用inline
可以在不影响代码可读性的前提下提升性能,但需结合性能分析工具验证实际效果。
所有权(ownership)
在编程中,所有权(Ownership) 是一个核心概念,尤其在系统级语言(如 C++/Rust)中至关重要。它定义了某个对象或资源的归属关系,明确谁负责资源的生命周期管理(如内存分配、释放、文件句柄关闭等)。
所有权的核心规则
- 唯一性:同一时刻,资源只能有一个明确的拥有者。
- 责任绑定:拥有者负责资源的创建和销毁。
- 转移控制:所有权可以通过特定操作(如移动语义)转移给其他实体。
C++ 中的所有权体现
1. 原始指针(无明确所有权)
- 问题:所有权不明确,容易导致泄漏或重复释放。
2. 引用
所有权是资源的归属权,引用被设计为一种临时、轻量级的资源访问机制,而非资源的管理者,它不会管理资源的生命周期。
3. 智能指针(显式所有权管理)
-
std::unique_ptr
(独占所有权): -
所有权唯一,不可复制,只能移动。
-
std::shared_ptr
(共享所有权): -
通过引用计数管理生命周期,所有权被多个对象共享。
所有权的核心作用
场景 | 所有权的意义 |
---|---|
内存安全 | 避免内存泄漏(忘记释放)或野指针(访问已释放内存)。 |
资源管理 | 统一管理文件句柄、网络连接、锁等资源,确保及时释放。 |
并发安全 | 明确所有权可防止多个线程同时修改同一资源,降低数据竞争风险。 |
代码可维护性 | 通过类型系统(如 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;
}
所有权的设计哲学
- RAII(资源获取即初始化)
- 资源生命周期绑定到对象作用域(如
unique_ptr
在析构时释放内存)。 - 最小权限原则
- 只让必要的代码拥有资源所有权,其他代码通过引用访问。
- 显式优于隐式
- 用类型系统(如智能指针)明确表达所有权,而非依赖约定。