C++ 基础语法
声明与定义
- 声明:只是说明名字的存在,外部声明使用
extern
关键字,函数可以不使用,因为不带函数体的函数名连同参数表或返回值,自动地作为一个声明。 -
定义:为名字分配内存,它可以同时是声明。
-
初始化:在变量被声明时给变量赋值,称变量被初始化。声明时未赋值称变量未初始化:
int a=0;
初始化,而int a;
未初始化。 -
C++中函数声明时可以仅给定参数类型而不给参数名称,在函数定义时再给定具体名称,在C中不行。
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来访问。
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;
的写法是错误的,因为声明时未初始化,不过在类中定义成员变量这是可以的,前提是:常量成员必须在构造函数中使用初始化列表进行初始化,且只能使用初始化列表初始化。例:
使用方式
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.h
和B.h
,而B.h
中也include了A.h
,那么A.h
就在main.cpp
中声明了两次。
作用域
命名空间(Namespace)
C++中的命名空间是一种用于组织代码、避免命名冲突的机制。
1. 命名空间的作用
- 避免命名冲突:将标识符(变量、函数、类等)封装在独立的命名空间中,防止不同库或模块中的同名标识符冲突。
- 组织代码结构:将相关代码分组,提高可读性和可维护性。
2. 定义命名空间
-
命名空间可以嵌套:
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. 其他特性
-
命名空间扩展:可在不同位置扩展命名空间内容。
-
全局命名空间:通过
::
访问全局作用域: -
内联命名空间(C++11):成员直接可见于外层命名空间,常用于版本控制:
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;
}
范围分解符
使用范围分解符/作用域解析符 ::
可以指定使用哪个范围(全局、类内、结构体内)的变量和函数,它是不可重载的运算符
全局范围::函数/变量名
:
缺省参数
缺省参数是在函数声明时就已给定的一个值,如果我们在调用函数时没有指定这一参数的值,编译器就会自动地插上这个值。注意:
- 一旦开始使用缺省参数,那么这个参数后面的所有参数都必须是缺省的。
- 默认参数的值可以在函数声明时或定义中指定,但不能在声明和定义中都给出默认值,否则就会报默认参数重复定义的错误。
// 函数声明:在声明时为参数指定默认值
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
关键字放在函数定义前(声明中不需要)。
-
类内直接定义的成员函数隐式内联:
3. 编译器如何处理内联?
- 建议而非强制:
inline
只是对编译器的优化建议,编译器可能拒绝内联(如递归函数、复杂函数)。 - 优化权衡:编译器会根据函数体大小、调用频率等因素决定是否内联。
- 跨编译单元可见性:内联函数需在每个使用它的编译单元中可见(通常定义在头文件中)。
4. 内联函数 vs 宏
特性 | 内联函数 | 宏 |
---|---|---|
类型安全 | ✔️ | ❌(文本替换) |
调试支持 | ✔️ | ❌(预处理器阶段展开) |
作用域 | 遵循C++作用域规则 | 无作用域概念 |
参数求值 | 参数只求值一次 | 可能多次求值(如MAX(x++) ) |
5. C++17扩展:内联变量
C++17允许在头文件中定义inline
变量,解决静态成员变量初始化问题:
6. 注意事项
- 代码膨胀:过度内联会增加二进制文件体积,可能降低缓存命中率。
- 调试困难:内联后的函数在调用栈中可能不可见。
- 虚函数不能内联:虚函数调用在运行时确定,无法静态展开。
- 递归函数通常无法内联(除非编译器进行尾递归优化)。
7. 最佳实践
- 优先让编译器决定:现代编译器能自动内联简单函数。
- 仅显式标记关键函数:如性能敏感的短函数。
- 头文件中定义内联函数:确保跨编译单元可见性。
- 避免复杂逻辑:循环、递归、大函数不适合内联。
- 结合
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;
}
错误用法
所有权(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
在析构时释放内存)。 - 最小权限原则
- 只让必要的代码拥有资源所有权,其他代码通过引用访问。
- 显式优于隐式
- 用类型系统(如智能指针)明确表达所有权,而非依赖约定。