C++入门

最简C++程序(HelloWorld)

#include <iostream>
// 引用std名字空间中的所有标识符
using namespace std;

int main()
{
    cout << "HelloWorld" << endl;
    return 0;
}

头文件

C++头文件与C语言头文件的核心差异与联系:

特性 C语言头文件 C++头文件
后缀要求 必须带.h(如math.h) 标准头文件无后缀(如iostream)
C++对C头文件的兼容 - 可兼容.h后缀,优先用c前缀版本(如cmath)
特殊后缀 支持.hpp后缀(自定义头文件常用)

名字空间

基本概念

名字空间本质是C++中的自定义作用域,设计初衷是解决大规模软件开发中,不同库/模块的全局符号(变量、函数、类等)重名冲突问题。通过为不同模块划分独立作用域,让重名符号可共存,提升编程便利性。

名字空间作用域隔离示意图

名字空间的定义与使用

定义规则

定义名字空间即定义一个独立作用域,可在其中声明/定义变量、函数、类等符号,语法格式:

// 语法模板
namespace 名字空间名 {
    // 变量、函数、类等的定义/声明
    数据类型 变量名;
    返回值类型 函数名(参数列表);
}
示例代码(分文件编写)

源文件(ns.cpp)- 定义名字空间内的符号

// ns.cpp
// 定义名字空间ns
namespace ns {
    // 定义变量
    int a = 1;
    // 定义函数
    float f(int x) {
        return x / 2.0f; // 修正:x/2改为x/2.0f,避免整数除法
    }
}

// 定义另一个名字空间another_ns
namespace another_ns {
    int a = 100; // 与ns中的a重名,但作用域隔离,无冲突
}

头文件(ns.h)- 声明名字空间内的符号

// ns.h
#ifndef NS_H // 补充头文件保护,避免重复包含
#define NS_H

namespace ns {
    // 声明符号(extern表示符号定义在其他文件)
    extern int a;
    extern float f(int x);
}

namespace another_ns {
    extern int a;
}

#endif // NS_H

主文件(main.cpp)- 引用名字空间内的符号

// main.cpp
#include <iostream>
#include "ns.h"
using std::cout; // 补充:避免直接导入整个std,减少名字污染
using std::endl;

int main() {
    // 使用【作用域引用符::】调用名字空间内的符号
    // 语法:名字空间名::符号名
    cout << ns::a << endl;         // 输出:1
    cout << another_ns::a << endl; // 输出:100
    cout << ns::f(8) << endl;      // 输出:4
    return 0;
}

核心语法:作用域引用符(::)

:: 称为作用域引用符,用于访问指定作用域内的符号,常见使用场景:

  • 名字空间::符号:访问名字空间内的变量/函数/类
  • 类::符号:访问类的静态成员/嵌套类型(后续学习)
  • 无前缀::符号:访问全局作用域的符号(例:::a 表示全局变量a)

对比:结构体/类的普通成员用 . 访问(如:struct_obj.member),作用域级符号用 :: 访问。

using语句:简化名字空间符号的引用

直接使用 名字空间::符号 会增加代码冗余,using 语句可简化引用,分为两种形式:

两种使用形式

形式 语法 特点
导入整个名字空间 using namespace 名字空间名; 便利,但易引发名字污染(未使用的符号成为冲突隐患)
导入指定符号 using 名字空间名::符号名; 精准,避免名字污染,推荐优先使用

示例对比

形式1:导入整个ns名字空间
// main.cpp
#include <iostream>
#include "ns.h"
using namespace ns; // 导入ns的所有符号
using std::cout;
using std::endl;

int main() {
    cout << a << endl;  // 等价于ns::a,输出1
    cout << f(8) << endl; // 等价于ns::f(8),输出4
    return 0;
}
形式2:导入ns的指定符号
// main.cpp
#include <iostream>
#include "ns.h"
using ns::a; // 仅导入ns::a
using ns::f; // 仅导入ns::f
using std::cout;
using std::endl;

int main() {
    cout << a << endl;  // 输出1
    cout << f(8) << endl; // 输出4
    return 0;
}

注意事项:避免名字空间污染

  • 标准名字空间std(C++标准库的所有符号都在std中):
    • 不推荐在头文件中写 using namespace std;(会让所有包含该头文件的代码都导入std,大幅增加冲突风险);
    • 源文件中若仅用少量std符号(如cout、endl),优先用 using std::cout; 而非导入整个std。

拓展

嵌套名字空间

名字空间可嵌套定义,访问时需逐层指定作用域,适用于大型项目的层级化管理(如按模块/功能划分)。

// 定义嵌套名字空间
namespace project {
    namespace module1 {
        int num = 10;
        void func() {
            std::cout << "module1::func" << std::endl;
        }
    }
    namespace module2 {
        int num = 20;
    }
}

// 访问嵌套名字空间的符号
int main() {
    // 方式1:逐层访问
    std::cout << project::module1::num << std::endl; // 输出10
    project::module1::func(); // 输出module1::func
    
    // 方式2:using简化
    using namespace project::module2;
    std::cout << num << std::endl; // 输出20
    return 0;
}

// C++17简化嵌套定义语法
namespace project::module3 { // 直接嵌套,无需多层{}
    int num = 30;
}

匿名名字空间

无名称的名字空间,仅在当前源文件内有效,等价于给符号加static(限制作用域为当前文件),避免全局符号跨文件冲突。

// 匿名名字空间(仅当前.cpp文件可见)
namespace {
    int local_num = 5; // 仅当前文件可访问,其他文件无法引用
    void local_func() {
        std::cout << "仅当前文件可见的函数" << std::endl;
    }
}

int main() {
    local_num = 10; // 可直接访问,无需作用域符
    local_func();   // 正常调用
    return 0;
}

内联名字空间(C++11)

inline修饰的名字空间,其符号可被外层名字空间直接访问,适用于版本兼容场景。

namespace lib {
    inline namespace v2 { // 内联名字空间v2
        void func() {
            std::cout << "lib v2::func" << std::endl;
        }
    }
    namespace v1 { // 非内联
        void func() {
            std::cout << "lib v1::func" << std::endl;
        }
    }
}

int main() {
    lib::func(); // 直接访问内联名字空间v2的func,输出lib v2::func
    lib::v1::func(); // 需显式访问v1的func,输出lib v1::func
    return 0;
}

名字空间的别名

为长名字空间定义短别名,简化代码书写,适用于嵌套/长名称的名字空间。

// 定义长名字空间
namespace very_long_namespace_name {
    int value = 100;
}

// 定义别名
namespace vlns = very_long_namespace_name;

// 嵌套名字空间的别名
namespace proj_mod1 = project::module1;

int main() {
    std::cout << vlns::value << std::endl; // 输出100
    std::cout << proj_mod1::num << std::endl; // 输出10
    return 0;
}

核心总结

  1. 名字空间核心作用:隔离全局符号,解决重名冲突,适配大规模软件开发;
  2. 访问符号的方式:名字空间::符号(精准)、using语句(简化);
  3. 最佳实践:
    • 头文件中避免using namespace xxx,防止名字污染;
    • 优先使用using 名字空间::符号而非导入整个名字空间;
    • 大型项目用嵌套名字空间做层级管理,用匿名名字空间限制文件内符号作用域;
  4. 拓展特性:嵌套、匿名、内联名字空间,名字空间别名,可按需提升代码可读性和管理效率。

标准输入、输出

C++的标准流对象替代C语言的printf/scanf,无需关注数据类型,更简洁灵活。所谓的标准流对象,指的是cout、cin和cerr。所谓对象,本质上就是C语言语境中的变量,只不过在面向对象的语境中,这些变量都是自带函数的,它们是名副其实的超级变量,因此为了与C语言的普通变量加以区分,在面向对象的编程语言中,通常将它们称为对象。

核心流对象

流对象 作用 特点
cout 标准输出(控制台) 经过行缓冲区,效率较高
cin 标准输入(键盘) 自动识别数据类型
cerr 标准错误输出 无缓冲区,实时输出
clog 标准日志输出 有缓冲区,适合日志打印

基础用法

// 输出
cout << 100 << 3.14 << "abcd" << endl;
// 输入
int a; double f; string s;
cin >> a >> f >> s;
标准输入输出流对象工作流程示意图

字符串string

C语言无原生字符串类型,依赖字符数组/指针;C++的std::string是系统原生支持的一种基本数据类型,提供丰富操作接口,安全且便捷。

核心基础操作

// 赋值与拷贝
string s1 = "abcd";
string s2 = s1; // 拷贝构造
s2 = s1;        // 赋值

// 拼接
string s3 = "xyz" + s1;
s3 += "123";    // 结果:xyzabcd123

// 比对
if (s1 == "abcd") cout << "相等" << endl;

// 元素修改
s1[1] = 'B';    // s1变为"aBcd"

常用接口分类

构造函数

string str1;                // 空字符串
string str2("Hello");       // 从C风格字符串构造
string str3(str2);          // 拷贝构造
string str4(5, 'A');        // 5个'A' → "AAAAA"
string str5("Hello", 3);    // 前3个字符 → "Hel"
string str6(str2, 1, 3);    // 索引1开始取3个 → "ell"

容量相关

string str = "Hello";
str.size();         // 长度:5(同length())
str.capacity();     // 分配的存储容量(大于等于size)
str.empty();        // 判断是否为空
str.resize(10);     // 调整长度,不足补'\0'
str.reserve(100);   // 预留容量,避免频繁扩容
str.shrink_to_fit();// 释放多余容量

元素访问

string str = "Hello";
str[0];             // 访问第0个字符(不检查边界)
str.at(0);          // 访问第0个字符(检查边界,越界抛异常)
str.front();        // 第一个字符 → 'H'
str.back();         // 最后一个字符 → 'o'
str.c_str();        // 转为C风格字符串(const char*)

修改操作

// 追加
str.append(" World");   // "Hello World"
str += "!";             // "Hello World!"
str.push_back('?');     // "Hello World!?"

// 插入/删除
str.insert(2, "xxx");   // 在索引2插入 → "Hexxxllo World!?"
str.erase(2, 3);        // 从索引2删除3个字符 → 恢复"Hello World!?"
str.clear();            // 清空字符串

// 替换
str.replace(6, 5, "C++");// 索引6开始替换5个字符 → "Hello C++!?"

查找与子串

string str = "Hello World";
size_t pos = str.find("World"); // 查找子串,返回索引6(未找到返回string::npos)
str.rfind("l");                // 从后向前查找'l',返回9
string sub = str.substr(6, 5); // 索引6开始取5个字符 → "World"

数值转换(C++11+)

// 字符串转数值
string str_num = "123.45";
int i = stoi("123");       // 转int
double d = stod(str_num);  // 转double

// 数值转字符串
string s_int = to_string(123);    // "123"
string s_double = to_string(3.14);// "3.140000"

拓展

  1. string与C风格字符串转换
// string → const char*
string s = "test";
const char* c_str = s.c_str();

// const char* → string
const char* c = "test";
string s2 = c;
  1. string的内存管理:string底层是动态字符数组,会自动扩容(通常按2倍扩容),reserve()可减少扩容次数,提升效率。
  2. 常见坑点[]访问越界不会报错,at()会抛异常;空字符串调用front()/back()会崩溃。
string底层内存结构示意图(可补充:扩容流程、字符存储方式)

引用

核心概念与语法

引用是“已有变量的别名”,与原变量共享同一块内存,语法:

int a = 100;
int &ra = a; // ra是a的别名,必须定义时赋值

// 操作引用等价于操作原变量
ra = 200; // a的值变为200

核心注意点

  1. 引用必须定义时赋值,不可单独定义(int &r; 错误);
  2. 引用关系一旦确立,无法修改(不能再指向其他变量);
  3. 引用不占用额外内存(与指针不同)。

拓展:引用 vs 指针

特性 引用 指针
定义赋值 必须定义时赋值 可先定义后赋值
指向修改 不可修改 可指向其他变量
内存占用 不占用额外内存 占用4/8字节(32/64位)
空值 无空引用 有空指针(NULL/nullptr)
语法复杂度 简洁(无解引用符) 需解引用符(*)

引用的实用场景

  1. 函数参数(避免拷贝,提升效率):
void swap(int &x, int &y) {
    int temp = x;
    x = y;
    y = temp;
}
  1. 函数返回值(避免拷贝,可修改返回值):
int& getVal(int arr[], int idx) {
    return arr[idx];
}
// 使用:getVal(arr, 0) = 100; // 直接修改数组第一个元素
引用与原变量内存映射示意图(可补充:指针与引用的内存对比)

类型转换

C++兼容C语言的旧式转换,同时新增更安全、更清晰的新式转换。

旧式转换(不推荐)

char a = 10;
// C语言风格
int b = (int)a;
// C++风格(仅语法调整,无安全性提升)
int c = int(a);

新式转换(推荐)

const_cast:去除const/volatile属性

仅作用于指针/引用,核心用途:修改const修饰的指针/引用指向的变量(注意:修改const变量本身未定义行为)。

// 处理const引用
const int &ri = 6;
const_cast<int &>(ri) = 8; // 可行

// 处理常目标指针
const int *p = &a;
*(const_cast<int *>(p)) = 8; // 可行

// 不可处理常指针本身(char *const p)
int *const p = &a;
const_cast<int *>(p) = &b; // 错误

static_cast:静态类型转换(编译期检查)

  • 等价于旧式转换,但可读性更高,且会检查类型兼容性;
  • 支持基本类型转换(int→float)、父子类上行转换(子类→父类);
  • 不支持不兼容类型转换(如int→float,会编译报错)。
float f = 3.14;
int i = static_cast<int>(f); // 3,安全且易读

int *pi = &i;
float *pf = static_cast<float*>(pi); // 编译报错,不兼容

dynamic_cast:动态类型转换(运行期检查)

专用于类的父子代际转换,仅支持指针/引用,且要求类有虚函数;

  • 子类→父类(上行转换):与static_cast等价;
  • 父类→子类(下行转换):运行期检查,转换失败返回NULL(指针)/抛异常(引用)。

reinterpret_cast(拓展)

最“强制”的转换,无视类型兼容性,仅重新解释内存布局(极少使用,风险高):

int a = 0x12345678;
char *p = reinterpret_cast<char*>(&a); // 直接将int内存解释为char*

转换使用原则

  1. 优先用新式转换,避免旧式转换;
  2. 非必要不使用const_cast(破坏const语义);
  3. 跨类型转换优先确认兼容性,避免reinterpret_cast。

关键字auto

核心功能

C++11后,auto不再是C语言的“栈变量标记”,而是自动推导变量类型,编译期确定类型。

auto a = 100;        // int
auto b = 3.14;       // double
auto c = "hello";    // const char*
auto d = string("hi");// string

核心使用场景

  1. 简化复杂类型(如容器迭代器):
vector<int> vec = {1,2,3};
auto it = vec.begin(); // 替代vector<int>::iterator
  1. 模板中推导未知类型:
template <typename T>
void show(T &container) {
    auto it = container.begin(); // 适配任意容器类型
}

auto的限制

  1. 不能作为函数参数类型(void f(auto a) 错误,需用模板);
  2. 不能用于类的非静态成员变量;
  3. 定义时必须初始化(auto a; 错误)。

命名规范(约定俗成)

命名规范无强制标准,但统一的规范可提升代码可读性,C++常用规范:

标识符类型 命名风格 示例
类名 大驼峰(帕斯卡) AppleTree、Student
变量/函数名 小驼峰 appleTree、getScore
常量 全大写+下划线 MAX_NUM、PI
全大写+下划线 #define MIN_VAL 0
名字空间 小写 namespace my_space
私有成员 后缀下划线 int age_;

拓展:团队规范优先遵循团队约定,无约定时按上述规范统一。

枚举循环(C++11 range-based for)

核心语法

专用于遍历有确定元素个数的数组、容器,无需关心循环次数:

// 数组遍历
int a[3] = {1,2,3};
for(auto i : a) {
    cout << i << endl; // 依次输出1、2、3
}

// 容器遍历(拓展)
vector<string> vec = {"a","b","c"};
for(auto &s : vec) { // 引用避免拷贝
    s += "123"; // 修改原容器元素
}

遍历注意点

  1. 普通遍历(auto i)是拷贝,修改i不影响原数据;
  2. 需修改原数据用引用(auto &i);
  3. 只读遍历用const引用(const auto &i),提升效率。

函数默认参数

核心语法

函数参数可指定默认值,调用时未传参则使用默认值:

// 声明时指定默认参数(推荐)
void f(int a, float b = 3.14);

int main() {
    f(100, 1.23); // 使用传入的b=1.23
    f(200);       // 使用默认的b=3.14
    return 0;
}

// 定义时不重复写默认参数
void f(int a, float b) {
    cout << a + b << endl;
}

核心规则

  1. 默认参数必须位于参数列表最右侧void f(int a=1, int b) 错误);
  2. 声明与定义分离时,仅声明指定默认参数(避免重复定义);
  3. 默认参数值必须是编译期常量(不能用变量)。

拓展:默认参数的坑点

默认参数在编译期确定,若默认参数依赖外部变量,修改变量不会影响默认值:

int val = 10;
void f(int a = val); // 默认值为10
val = 20;
f(); // 仍使用10,而非20

函数重载

核心概念

同一作用域内,函数名相同、参数列表不同的函数可共存,编译器根据参数区分调用版本(静态多态)。

可形成重载的情形

  1. 参数个数不同:
void f1(int a);
void f1(int a, int b);
  1. 参数类型不同:
void f2(int a);
void f2(float b);
  1. 类方法的const属性不同:
class A {
    void f3() const; // const版本
    void f3();       // 非const版本
};
  1. 普通指针与常目标指针不同:
void f4(char *p);
void f4(const char *p);

不可形成重载的情形

  1. 仅返回值类型不同:
void f1(int a);
float f1(int a); // 冲突,无法重载
  1. 仅static修饰符不同:
int f2(int a);
static int f2(int a); // 冲突
  1. 仅参数的const修饰(非指针/引用):
void f3(int a);
void f3(const int a); // 冲突(a是值传递,const无意义)
  1. 仅常指针本身(非常目标指针):
void f4(char *p);
void f4(char *const p); // 冲突(const修饰指针本身,参数列表无差异)

重载的底层原理(名字改编 Name Mangling)

编译器会根据函数名+参数列表生成唯一的“编译名”,例如:

  • void f(int)_Z1fi
  • void f(float)_Z1ff
    C语言无名字改编,因此不支持重载(可通过extern "C"禁用C++名字改编)。

堆内存管理

C++兼容C的malloc/free,但新增new/delete,更适配面向对象特性。

核心语法对比

操作 C语言 C++语言
分配单个变量 int *p = malloc(sizeof(int)); int *p = new int;
分配数组 int *p = calloc(100, sizeof(int)); int *p = new int[100];
释放单个变量 free(p); delete p;
释放数组 free(p); delete [] p;(必须加[])

核心差异(类对象场景)

操作 malloc/free new/delete
分配时 仅分配内存,不调用构造函数 分配内存+自动调用构造函数(初始化)
释放时 仅释放内存,不调用析构函数 调用析构函数(释放资源)+释放内存

示例(类对象):

class Test {
public:
    Test() { cout << "构造函数" << endl; }
    ~Test() { cout << "析构函数" << endl; }
};

int main() {
    Test *p1 = (Test*)malloc(sizeof(Test)); // 无构造函数调用
    free(p1); // 无析构函数调用

    Test *p2 = new Test; // 调用构造函数
    delete p2; // 调用析构函数
    return 0;
}

拓展知识点

  1. 内存泄漏:new/delete未成对使用会导致内存泄漏,可使用智能指针(unique_ptr/shared_ptr)避免;
  2. new失败:早期C++返回NULL,C++11后抛bad_alloc异常;
  3. 不能混用mallocdeletenewfree(行为未定义)。
new/delete与malloc/free内存管理流程对比图

C++三大核心特性(基础认知)

特性 核心含义 细分类型
封装 将数据与操作数据的函数封装为类,隐藏内部实现,仅暴露接口 -
继承 子类复用父类的属性和方法,可拓展或重写父类逻辑 -
多态 同一接口表现不同行为 静态多态(编译期):重载
动态多态(运行期):虚函数

posted @ 2025-12-15 08:51  Jaklin  阅读(56)  评论(0)    收藏  举报