[C++] 初步认识

📅 2026/7/5 14:42:48
[C++] 初步认识
目录1.命名空间namespace1.1namespace的价值1.2namespace 的定义与使用1.2.1 定义1.2.2 使用2.C输入输出3.缺省参数4.函数重载5.引用 /const引用与权限 引用与指针的关系5.1 引用的概念和定义5.2引用的特性5.3 引用的使用5.4 const引用5.4.1临时对象存在的场景5.5指针与引用的关系6.inline 内联7.nullptrC兼容C语言的绝大部分语法特性同时作为C语言的扩展版本它在C的基础上引入了更多功能并解决了C语言的部分局限性。这些改进使得C的语法体系更加完善提升了代码的表达能力和开发效率。后文将系统介绍C的核心特性与设计理念。1.命名空间namespace1.1namespace的价值在C中变量、函数以及C中新增的类都是⼤量存在的这些变量、函数和类的名称都存在于全局作⽤域中可能会导致命名冲突或名字污染。使⽤namespace的⽬的是对标识符的名称进⾏本地化以避免这些问题。C语⾔项⽬类似下⾯程序这样的命名冲突是普遍存在的问题C引⼊namespace就是为了更好的解决这种问题#include stdio.h #include stdlib.h int rand 10; int main() { // 编译报错error C2365: “rand”: 重定义以前的定义是“函数” printf(%d\n, rand); return 0; }1.2namespace 的定义与使用1.2.1 定义namespace作为一个关键字后⾯紧跟命名空间的名字然后接⼀对{ }即可{ }中即为命名空间的成员。命名空间中可以定义变量/函数/类型等。namespace本质是定义出⼀个域这个域跟全局域各自独立不同的域可以定义同名变量所以下⾯的rand就不存在冲突了。C中域有函数局部域全局域命名空间域类域域影响的是编译时语法查找⼀个变量/函数/类型出处(声明或定义)的逻辑所以有了域隔离名字冲突就解决了。局部域和全局域除了会影响编译查找逻辑还会影响变量的⽣命周期命名空间域和类域不影响变量⽣命周期。namespace只能定义在全局当然他还可以嵌套定义。项⽬⼯程中多⽂件中定义的同名namespace会认为是⼀个namespace不会冲突。C标准库都放在⼀个叫std(standard)的命名空间中。#include stdio.h #include stdlib.h // 1. 正常的命名空间定义 // space是命名空间的名字⼀般开发中是⽤项⽬名字做命名空间名。 namespace space { // 命名空间中可以定义变量/函数/类型 int rand 10; int Add(int left, int right) { return left right; } struct Node { struct Node* next; int val; }; } int main() { // 这⾥默认是访问的是全局的rand函数指针 printf(%p\n, rand); // 这⾥指定space命名空间中的rand printf(%d\n, bit::rand); //::是域访问操作符 return 0; } //2. 命名空间可以嵌套 namespace space { namespace zhangsan { int rand 1; int Add(int left, int right) { return left right; } } namespace lisi { int rand 2; int Add(int left, int right) { return (left right)*10; } } } int main() { printf(%d\n, space::zhangsan::rand); printf(%d\n, space::lisi::rand); printf(%d\n, space::zhangsan::Add(1, 2)); printf(%d\n, space::lisi::Add(1, 2)); return 0; }1.2.2 使用编译查找⼀个变量的声明/定义时默认只会在局部或者全局查找不会到命名空间里面去查找。所以下面程序会编译报错。如果我们要使用命名空间中定义的变量/函数有三种方式一、指定命名空间访问项目中推荐这种方式。二、using将命名空间中某个成员展开项目中经常访问且不存在冲突的成员推荐这种⽅式。三、展开命名空间中全部成员项目不推荐冲突风险很大日常小练习程序为了放便推荐使用。#include stdio.h namespace N { int a 0; int b 1; } int main() { // 编译报错error C2065: “a”: 未声明的标识符 printf(%d\n, a);// 查找顺序局部域-全局域 return 0; } // 指定命名空间访问 int main() { printf(%d\n, N::a); return 0; } // using将命名空间中某个成员展开 using N::b; int main() { printf(%d\n, N::a); printf(%d\n, b); return 0; } // 展开命名空间中全部成员 using namespce N; int main() { printf(%d\n, a); printf(%d\n, b); return 0; }2.C输入输出iostream 是Input Output Stream的缩写是标准的输⼊、输出流库定义了标准的输⼊键盘、输出屏幕对象。std::cin 和 std::cout 分别是istream 类和ostream 类的对象主要⾯向窄字符narrow characters of type char)的标准输⼊/输出流。C中窄字符特指单字节字符如ASCII字符std::endl 是⼀个操控符流插⼊输出时相当于插⼊⼀个换行字符加刷新缓冲区与cout搭配使用。是流插⼊运算符与cout搭配是流提取运算符与cin搭配。C语⾔还⽤这两个运算符做位运算左移/右移使⽤C输⼊输出更⽅便不需要像printf/scanf输⼊输出时那样需要⼿动指定格式C的输⼊输出可以⾃动识别变量类型(本质是通过函数重载实现的这个下面就会讲到)其实最重要的是C的流能更好的⽀持自定义类型对象的输入输出。⼀般⽇常练习中我们可以using namespace std实际项⽬开发中不建议using namespace std。这⾥我们没有包含stdio.h也可以使⽤printf和scanf在包含iostream间接包含了。vs系列编译器是这样的其他编译器可能会报错。#define _CRT_SECURE_NO_WARNINGS 1 #include iostream using namespace std; int main() { int a 0; double b 0.1; char c x; cout a b c endl; scanf(%d%lf, a, b); printf(%d %lf\n, a, b); // 可以⾃动识别变量的类型 cin a; cin b c; cout a endl; cout b c endl; return 0; } // 在io需求⽐较⾼的地⽅加上以下3⾏代码可以提⾼CIO效率 ios_base::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);3.缺省参数缺省参数是声明或定义函数时为函数的参数指定⼀个缺省值。在调⽤该函数时如果没有指定实参则采⽤该形参的缺省值否则使⽤指定的实参缺省参数分为全缺省和半缺省参数。缺省参数也叫默认参数全缺省就是全部形参给缺省值半缺省就是部分形参给缺省值。C规定半缺省参数必须从右往左依次连续缺省不能间隔跳跃给缺省值。带缺省参数的函数调⽤C规定必须从左到右依次给实参不能跳跃给实参。函数声明和定义分离时缺省参数不能在函数声明和定义中同时出现规定必须函数声明给缺省值。#include iostream #include assert.h using namespace std; void Func(int a 0) { cout a endl; } int main() { Func(); // 没有传参时使⽤参数的默认值 Func(10); // 传参时使⽤指定的实参 return 0; } #include iostream using namespace std; // 全缺省 void Func1(int a 10, int b 20, int c 30) { cout a a endl; cout b b endl; cout c c endl endl; } // 半缺省 void Func2(int a, int b 10, int c 20) { cout a a endl; cout b b endl; cout c c endl endl; } int main() { Func1(); Func1(1); Func1(1,2); Func1(1,2,3); Func2(100); Func2(100, 200); Func2(100, 200, 300); return 0; }4.函数重载C⽀持在同⼀作⽤域中出现同名函数但是要求这些同名函数的形参不同可以是参数个数不同或者类型不同。这样C函数调⽤就表现出了多态⾏为使⽤更灵活。#includeiostream using namespace std; // 1、参数类型不同 int Add(int left, int right) { cout int Add(int left, int right) endl; return left right; } double Add(double left, double right) { cout double Add(double left, double right) endl; return left right; } // 2、参数个数不同 void f() { cout f() endl; } void f(int a) { cout f(int a) endl; } // 返回值不同不能作为重载条件因为调⽤时⽆法区分 //void fxx() //{} // //int fxx() //{ // return 0; //} // 下⾯两个函数构成重载 // 但是调⽤时会报错存在歧义编译器不知道调⽤谁 void f1() { cout f() endl; } void f1(int a 10) { cout f(int a) endl; } int main() { Add(10, 20); Add(10.1, 20.2); f(); f(10); f1(); //报错 return 0; }5.引用 /const引用与权限 引用与指针的关系5.1 引用的概念和定义引⽤不是新定义⼀个变量⽽是给已存在变量取了⼀个别名编译器不会为引用变量开辟内存空间它和它引用的变量共⽤同⼀块内存空间。类型 引用别名 引用对象#includeiostream using namespace std; int main() { int a 0; // 引⽤b和c是a的别名 int b a; int c a; // 也可以给别名b取别名d相当于还是a的别名 int d b; d; // 这⾥取地址我们看到是⼀样的 cout a endl; cout b endl; cout c endl; cout d endl; return 0; }5.2引用的特性引用在定义时必须初始化⼀个变量可以有多个引用引用一旦引用⼀个实体再不能引用其他实体#includeiostream using namespace std; int main() { int a 10; // 编译报错“ra”: 必须初始化引⽤ //int ra; int b a; int c 20; // 这⾥并⾮让b引⽤c因为C引⽤不能改变指向 // 这⾥是⼀个赋值 b c; cout a endl; cout b endl; cout c endl; return 0; }5.3 引用的使用引用在实践中主要是于引用传参和引用做返回值中减少拷贝提高效率和改变引用对象时同时改变被引用对象。引用传参跟指针传参功能是类似的引用传参相对更方便⼀些。引用和指针在实践中相辅相成功能有重叠性但是各有特点互相不可替代。C的引跟其他语言的引用(如Java)是有很⼤的区别的除了用法最大的点C引用定义后不能改变指向Java的引用可以改变指向。void Swap(int rx, int ry) { int tmp rx; rx ry; ry tmp; } int main() { int x 0, y 1; cout x y endl; Swap(x, y); cout x y endl; return 0; }5.4 const引用可以引用⼀个const对象但是必须用const引用。const引用也可以引用普通对象因为对象的访问权限在引用过程中可以缩小但是不能放大。需要注意的是类似 int rb a*3; double d 12.34; int rd d; 这样⼀些场景下a*3的和结果保存在⼀个临时对象中 int rd d 也是类似在类型转换中会产生临时对象存储中间值也就是rb和rd引⽤用的都是临时对象而C规定临时对象具有常性所以这里就触发了权限放大必须要用常引用才可以。所谓临时对象就是编译器需要⼀个空间暂存表达式的求值结果时临时创建的⼀个未命名的对象C中把这个未命名对象叫做临时对象。int main() { const int a 10; // 编译报错error C2440: “初始化”: ⽆法从“const int”转换为“int ” // 这⾥的引⽤是对a访问权限的放⼤ //int ra a; // 这样才可以 const int ra a; // 编译报错error C3892: “ra”: 不能给常量赋值 //ra; // 这⾥的引⽤是对b访问权限的缩⼩ int b 20; const int rb b; // 编译报错error C3892: “rb”: 不能给常量赋值 //rb; return 0; } #includeiostream using namespace std; int main() { int a 10; const int ra 30; // 编译报错: “初始化”: ⽆法从“int”转换为“int ” // int rb a * 3; const int rb a*3; double d 12.34; // 编译报错“初始化”: ⽆法从“double”转换为“int ” // int rd d; const int rd d; return 0; }5.4.1临时对象存在的场景隐式类型转换、函数传参、函数传返回值、表达式求值a b、a*3)、自定义类型的传值5.5指针与引用的关系语法概念上引用是⼀个变量的取别名不开空间指针是存储⼀个变量地址要开空间。引用在定义时必须初始化指针建议初始化但是语法上不是必须的。引用在初始化时引用⼀个对象后就不能再引用其他对象而指针可以在不断地改变指向对象。引用可以直接访问指向对象指针需要解引用才是访问指向对象。sizeof中含义不同引用结果为引用类型的大小但指针始终是地址空间所占字节个数(32平台下占4个字节64位下是8字节)指针很容易出现空指针和野指针的问题引用很少出现引用使用起来相对更安全⼀些。6.inline 内联用inline修饰的函数叫做内联函数编译时C编译器会在调用的地放展开内联函数这样调⽤内联函数就需要建立栈帧了就可以提高效率。inline对于编译器而言只是⼀个建议也就是说你加了inline编译器也可以选择在调用的地方不展开不同编译器关于inline什么情况展开各不相同因为C标准没有规定这个。inline适⽤于频繁调应的短小函数对于递归函数代码相对多⼀些的函数加上inline也会被编译器忽略。C语⾔实现宏函数也会在预处理时替换展开但是宏函数实现很复杂很容易出错的且不⽅便调试C设计了inline目的就是替代C的宏函数。vs编译器debug版本下面默认是不展开inline的这样方便调试。inline不建议声明和定义分离到两个⽂件分离会导致链接错误。因为inline被展开就没有函数地址链接时会出现报错。#includeiostream using namespace std; inline int Add(int x, int y) { int ret x y; ret 1; ret 1; ret 1; return ret; } int main() { // 可以通过汇编观察程序是否展开 // 有call Add语句就是没有展开没有就是展开了 int ret Add(1, 2); cout Add(1, 2) * 5 endl; return 0; }7.nullptrNULL实际是⼀个宏在传统的C头⽂件(stddef.h)中可以看到如下代码#ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif #endifC中NULL可能被定义为字面常量0或者C中被定义为无类型指针(void*)的常量。不论采取何种定义在使用空值的指针时都不可避免的会遇到⼀些麻烦本想通过f(NULL)调用指针版本的f(int*)函数但是由于NULL被定义成0调⽤了f(int x)因此与程序的初衷相悖。f((void*)NULL);调用会报错。C11中引入nullptrnullptr是⼀个特殊的关键字nullptr是⼀种特殊类型的字⾯量它可以转换成任意其他类型的指针类型。使⽤nullptr定义空指针可以避免类型转换的问题因为nullptr只能被隐式地转换为指针类型⽽不能被转换为整数类型。#includeiostream using namespace std; void f(int x) { cout f(int x) endl; } void f(int* ptr) { cout f(int* ptr) endl; } int main() { f(0); // 本想通过f(NULL)调⽤指针版本的f(int*)函数但是由于NULL被定义成0 //调⽤了f(int x)因此与程序的初衷相悖。 f(NULL); f((int*)NULL); // 编译报错error C2665: “f”: 2 个重载中没有⼀个可以转换所有参数类型 // f((void*)NULL); f(nullptr); return 0; }完~