资讯动态

C++语言原理与实践(一):C++入门基础知识

发布时间:2026/9/18 20:14:08 来源:尧图企业网站定制
一. 标准输入输出输入输出C 使用iostream 库进行输入输出要包含头文件其包含两个基础类型istream与ostream分别代表输入流与输出流与 C 语言的 printf 和 scanf 不同C 使用的是流 (Stream)的概念。cout标准输出流通常是屏幕为ostream类型对象。cin标准输入流通常是键盘为istream类型对象。流插入运算符用于输出。流提取运算符用于输入。例如:#include iostream int main() { int age; std::cout 请输入您的年龄: ; std::cin age; // 从键盘读取数据赋值给 age std::cout 您今年 age 岁。 std::endl; return 0; }解释前缀std::指出cincoutendl是定义在名为std的命名空间中的而::为作用域运算符来指出我们想使用定义在std中的名字。注意这⾥我们没有包含也可以使⽤printf和scanf在包含间接包含了。vs系列编译器是这样的其他编译器可能会报错。易错点方向写反新手常把 cin 写成 cin 。记住箭头指向数据流动的方向cin 的数据流向变量变量的数据流向 cout。缺少类型安全性虽然 cin 会自动识别类型但如果用户输入了非法字符如要求输入整数却输入了字母cin 会进入错误状态后续输入将失效。二. 命名空间1.namespace的价值这里可能会有人有疑问为什么需要这个命名空间下面以一个示例来演示一下在Test.cpp中#include stdio.h //#include stdlib.h int rand 10; int main() { printf(%d\n, rand); return 0; }我们运行时发现这个可以正常的运行倘若将这个头文件的注释删除了呢再次运行就会发现一下的报错原因全局变量rand与标准库函数rand()重名在C语言中函数名和普通变量名位于同一个标识符命名空间。同一作用域下rand不能既表示函数又表示变量。所以C引⼊namespace就是为了更好的解决这样的命名冲突问题。2.namespace的定义2.1.使用有一下的规则1.定义命名空间需要使⽤到namespace关键字后面跟命名空间的名字然后接⼀对{}即可{}中 即为命名空间的成员。命名空间中可以定义变量/函数/类型等。例如namespace bit { int rand 10; }2.namespace本质是定义出⼀个域这个域跟全局域各⾃独⽴不同的域可以定义同名变量所以下面的rand不在冲突了如代码所示#includeiostream using namespace std; namespace kong { int rand 10; } int main() { cout rand endl; return 0; }运行结果为什么不是我们想要的结果为10的rand却是这个16进制的随机值呢原因rand被放在kong命名空间中而main位于全局作用域编译器不会主动进入kong命名空间查找。因此cout rand找到的是标准库中的rand函数。由于没有添加()函数并未被调用输出的是函数地址。命名空间中的成员通常需要通过“命名空间名 ::”访问例如kong::rand。我们向这个命名空间kong里面添加一些内容方便演示namespace kong { int rand 10; int Add(int left, int right) { return left right; } }int main() { cout kong::Add(1, 2) endl; cout kong::rand endl; return 0; }运行结果3.namespace只能定义在全局当然他还可以嵌套定义例如namespace kong { namespace shao { int rand 1; int Add(int left, int right) { return left right; } } namespace ou { int rand 2; int Add(int left, int right) { return (left right) * 10; } } }int main() { cout kong::rand endl; cout kong::shao::rand endl; cout kong::ou::rand endl; }运行结果4.项⽬⼯程中多⽂件中定义的同名namespace会认为是⼀个namespace不会冲突。注意C标准库都放在⼀个叫std(standard)的命名空间中在项目使用中最好不要展开std至于什么是展开命名空间下面会讲。2.2.命名空间的展开通过上面的例子演示我们会发现一个问题明明cout和endl都在std命名空间里面啊但是在使用时却没有用到::这就要提到命名空间的展开了。1.指定命名空间访问项⽬中推荐这种方式。#include iostream namespace my_space { int a 10; void print() { std::cout From my_space std::endl; } } int main() { // 显式指定命名空间访问 std::cout my_space::a std::endl; my_space::print(); return 0; }但是这样使用的话我们就要不断的使用std::了。2.using将命名空间中某个成员展开项⽬中经常访问的不存在冲突的成员推荐这种方式。#include iostream namespace my_space { int a 10; int b 20; void print() { std::cout From my_space std::endl; } } // 仅展开需要的成员 using my_space::a; using std::cout; //展开std中的cout但是使用endl时还需要加std::的 int main() { // 直接使用a和cout cout a std::endl; // 未展开的成员需要显式访问 cout my_space::b std::endl; my_space::print(); return 0; }3.展开命名空间中全部成员项目不推荐冲突风险很大日常小练习程序为了方便推荐使用。#include iostream //全部展开std using namespace std; namespace my_space { int a 10; void print() { cout From my_space endl; } } // 全部展开 my_space using namespace my_space; int main() { // 直接使用无需 my_space:: cout a endl; print(); return 0; }注意using namespace std在小型练习中没问题但在大型项目中如果你的变量名恰好和标准库里的某个函数同名会导致难以察觉的编译错误。三.缺省参数缺省参数是声明或定义函数时为函数的参数指定一个缺省值。使用规则:1.在调用该函数时如果没有指定实参则采用该形参的缺省值否则使用指定的实参缺省参数分为全缺省和半缺省参数。例如#include iostream using namespace std; // 全缺省三个参数都给了默认值 void func(int a 10, int b 20, int c 30) { cout a a , b b , c c endl; } int main() { func(); // 不传参a10, b20, c30 func(1); // 传1个a1, b20, c30 func(1, 2); // 传2个a1, b2, c30 func(1, 2, 3); // 传3个a1, b2, c3 return 0; }运行结果但是像下面这样写就是不行的func(1, , 2);2.全缺省就是全部形参给缺省值半缺省就是部分形参给缺省值。C规定半缺省参数必须从右往左依次连续缺省不能间隔跳跃给缺省值。例如#include iostream using namespace std; // 半缺省b和c给默认值a不给 → 合法从右往左连续 void func(int a, int b 20, int c 30) { cout a a , b b , c c endl; } // ❌ 错误示例a给默认值b不给c给 → 不合法不是从右往左连续 // void func(int a 10, int b, int c 30) { ... } int main() { func(1); // 传1个a1, b20, c30 func(1, 2); // 传2个a1, b2, c30 func(1, 2, 3); // 传3个a1, b2, c3 // func(); // ❌ 错误a没有默认值必须传参 return 0; }运行结果3.带缺省参数的函数调用C规定必须从左到右依次给实参不能跳跃给实参。例如#include iostream using namespace std; void func(int a, int b 20, int c 30) { cout a a , b b , c c endl; } int main() { func(1); // ✅ 合法传ab和c用默认 func(1, 2); // ✅ 合法传a、bc用默认 // func(1, , 3); // ❌ 错误不能跳过b直接传c return 0; }运行结果4.函数声明和定义分离时缺省参数不能在函数声明和定义中同时出现规定必须函数声明给缺省值。例如// 在test.h中声明缺省参数写在这里 #include iostream using namespace std; void func(int a 10, int b 20); // 在test.cpp中定义不能再写缺省参数 void func(int a, int b) { cout a a , b b endl; } //这个是main.cpp中的代码 #includetest.h int main() { func(); // ✅ 合法a10, b20 func(5); // ✅ 合法a5, b20 return 0; }四. 引用1.使用在 C 的引用语法里是引用声明符它的作用是告诉编译器当前定义的变量是一个“引用”即另一个变量的别名与原变量共处一块空间。语法类型引用名原变量例如#include iostream using namespace std; int main() { int a10; intraa; intrbra; intrcrb; couta ra rb rcendl; return 0; }运行结果上面的rarbrc均为a的别名。作用:可以用于引用传参和引用作返回值来减少拷贝提高效率。2.特点1.定义时必须初始化int a 10; int ref a; //正确定义时初始化 // int ref; //错误未初始化编译报错2.一个变量可以有多个引用int a 10; int ref1 a; int ref2 a; //正确a有两个引用ref1、ref2 ref1 20; cout a ref2 endl; // 输出 20 203.引用一旦引用一个实体就不能引用其它的实体int a 10; int b 20; int ref a; // 绑定a ref b; // 不是绑定b而是把b的值赋给ref即 a 20 cout a endl; // 输出 20 cout ref a endl; // 地址相同仍绑定a3.引用的使用价值实践中主要是用于引用传参和传引用返回中减少拷贝提高效率和改变引用对象时同时改变被引用对象。例如交换函数中的参数就可以用引用#include iostream using namespace std; 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; }函数返回的时候也可以用引用下面以一个简单的例子来举例#include iostream #include assert.h using namespace std; typedef struct Stack { int* a; int top; int capacity; } ST; void STInit(ST rs, int n 100) { rs.a (int*)malloc(n * sizeof(int)); rs.top 0; rs.capacity n; } // 栈顶,为了方便,就不考虑扩容了毕竟仅插入几个数据而已 void STPush(ST rs,int x) { rs.a[rs.top] x; rs.top; } // int STTop(ST rs) int STTop(ST rs) { assert(rs.top 0); return rs.a[rs.top-1]; } int main() { // 调用全局的 ST st1; STInit(st1); STPush(st1, 1); STPush(st1, 2); int x STTop(st1); STTop(st1) 10; cout STTop(st1) endl; cout x endl; return 0; }运行结果原因我们这个是传值返回返回时会把数组元素复制一份STTop(st1)得到的不是a[1]本身而是它的一份临时对象属于右值而需要修改左边的对象所以它的左操作数必须是一个“可修改的左值”所以此时我们可以将STTop修改成// int STTop(ST rs) int STTop(ST rs) { assert(rs.top 0); return rs.a[rs.top-1]; }运行结果4.const引用特点1.可以引用一个const对象但是必须用const引用。const引用也可以引用普通对象因为对象的访问权限在引用过程中可以缩小但是不能放大。#include iostream using namespace std; int main() { int a 10; // int rb a * 3; //编译报错无法绑定临时对象到非const引用 const int rb a * 3;//合法const引用接受带常性的临时对象 cout rb endl; // 输出30临时对象生命周期随const引用延长 return 0; }2.不过需要注意的是类似int rba*3;double d12.34;int rdd这样一些场景下a*3的和结果保存在一个临时对象中intrdd也是类似在类型转换中会产生临时对象存储中间值也就是rb和rd引用的都是临时对象而C规定临时对象具有常性所以这里就触发了权限放大必须要用常引用才可以。#include iostream using namespace std; int main() { double d 12.34; // int rd d;//编译报错double转int生成临时对象非const引用不能绑 const int rd d;//合法const引用绑定带常性的临时对象 cout rd endl;// 输出12取临时对象的值类型转换后结果 return 0; }3.所谓临时对象就是编译器需要一个空间暂存表达式的求值结果时临时创建的一个未命名的对象 C中把这个未命名对象叫做临时对象。注意#include iostream using namespace std; int main() { int a 20; // 普通对象可读可写 const int ca a;//合法权限从可写→只读缩小允许 cout ca endl; //输出20 a 30; // 原对象仍可改ca会同步变共享内存 cout ca endl;//输出30 ✅ return 0; }总结所有生成临时对象的场景表达式计算、类型转换临时对象都有常性只能用const引用绑定const引用绑普通对象本质是主动缩小访问权限完全合法。5.指针与引用的区别• 语法概念上引用是⼀个变量的取别名不开空间指针是存储⼀个变量地址要开空间。• 引用在定义时必须初始化指针建议初始化但是语法上不是必须的。• 引用在初始化时引用⼀个对象后就不能再引用其他对象⽽指针可以在不断地改变指向对象。 • 引⽤可以直接访问指向对象指针需要解引⽤才是访问指向对象。• sizeof中含义不同引用结果为引用类型的大小但指针始终是地址空间所占字节个数(32位平台下 占4个字节64位下是8byte)• 指针很容易出现空指针和野指针的问题引⽤很少出现引用使用起来相对更安全⼀些。五.函数重载特点在C中 允许你使用同一个函数名来定义多个功能相似但参数不同的函数在C语言中如果想写一个加法函数可能会遇到这样的情况处理整数add_int(int a, int b)处理浮点数add_float(float a, float b)处理三个整数add_three_ints(int a, int b, int c)但在C中一个方程式就可以表示这三个。核心定义指在同一个作用域内可以有一组函数名相同但参数列表不同的函数。要构成重载函数的参数列表必须满足以下至少一个条件不同1.参数类型不同void func(int a)和void func(double a)。例如#include iostream using namespace std; // 版本 1处理两个整数 int add(int a, int b) { return a b; } // 版本 2处理两个浮点数 double add(double a, double b) { return a b; } int main() { // 编译器会根据你传的参数类型自动“点菜” cout 1 2 add(1, 2) endl; // 匹配版本 1 cout 1.5 2.3 add(1.5, 2.3) endl; // 匹配版本 2 return 0; }运行结果2.参数个数不同void func(int a)和void func(int a, int b)。例如#includeiostream using namespace std; int fun1(int x,int y) { return xy; } int fun1(int x,int y,int z) { return xyz; } int main() { int ret1fun1(4,6); int ret2fun1(4,5,6); coutret1ret1endl; coutret2ret2endl; return 0; }运行结果3.参数顺序不同void func(int a, double b)和void func(double a, int b)。#include iostream using namespace std; // 版本 A顺序是 (字符, 整数) void show(char level, int id) { cout 等级是: level , 编号是: id endl; } // 版本 B顺序是 (整数, 字符) —— 顺序换了构成了重载 void show(int id, char level) { cout 编号是: id , 等级是: level endl; } int main() { // 编译器根据你传参数的顺序来自动选择 show(A, 101); // 匹配 (char, int) - 调用版本 A show(102, B); // 匹配 (int, char) - 调用版本 B return 0; }运行结果注意1只有返回值类型不同是无法构成函数重载的。例如#include iostream // 函数 1返回整数 int getData(int x) { return x; } // 函数 2返回小数 // ❌ 报错编译器会提示“无法重载仅按返回类型区分的函数” double getData(int x) { return (double)x; } int main() { //这样调用编译器该选哪一个呢 getData(10); return 0; }运行结果注意2当重载函数中的参数设置了默认值时要注意它是否会与其他重载函数形成相同的调用方式例如#includeiostream void f1() { cout f() endl; } void f1(int a 10) { cout f(int a) endl; } int main() { f1(); return 0; }运行结果原因编译器无法判断应该调用哪一个因此产生函数调用二义性错误。六.nullptr关键字在C语言中NULL的完整定义形式为#ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif #endif在 C 语言中void*可以自动转换为任何类型的指针所以用((void *)0)非常安全且确 C语言中C 具有更严格的类型检查不允许void*隐式转换成其他指针比如int*。如果 C 也用((void *)0)那么int* p NULL;就会报错。因此C 只能折中把NULL定义为整数0。例如在C中有这样的代码#includeiostream using namespace std; void fun(int x) { coutfun1(x)endl; } void fun(int*ptr) { coutfun2(ptr)endl; } int main() { fun(0);//调用第一个函数 fun(NULL); //在C中NULL会被确认为0所以编译器可能会报错或者调用第一个函数 return 0; }运行结果nullptr关键字是一个指针字面量属于一种特殊的类型std::nullptr_t它不是整数但可以安全地转换为任何类型的指针而不是整数类型。此时上面的代码可以改为#includeiostream using namespace std; void fun(int x) { coutfun1(x)endl; } void fun(int*ptr) { coutfun2(ptr)endl; } int main() { fun(0);//调用第一个函数 fun(nullptr);//调用第二个函数 return 0; }运行结果七.inline函数在 C 语言/C中宏#define本质上是一种简单的文本替换机制。然而如果使用不当很容易在运算时产生意想不到的错误。例如#include iostream #define SQUARE(x) x * x int main() { // 期望的结果是 (12) * (12) 9 // 实际替换结果1 2 * 1 2 5 int result SQUARE(1 2); cout result endl; return 0; }运行结果修正方案 给参数和结果加括号修改成#define SQUARE(x) ((x) * (x))。但是就算给参数和结果加括号也可能会出错例如#include iostream using namespace std; #define MAX(a, b) ((a) (b) ? (a) : (b)) int main() { int i 5, j 3; // 你期望 i 变成 6结果返回 6 // 实际替换结果((i) (j) ? (i) : (j)) int res MAX(i, j); // 此时 i 竟然变成了 7因为它在比较时加了一次在返回结果时又加了一次。 return 0; }运行结果针对以上的错误inline函数就可以解决。vs编译器debug版本下面默认是不展开inline的这样方便调试debug版本想展开需要设置⼀下 以下两个地⽅如图内联函数语法形式inline 返回类型 函数名(参数列表) { // 函数体 ... return 返回值; }特点1.编译器在编译阶段将调用处的函数名直接替换为完整的函数体代码。2.由于没有了函数调用的参数传递、栈帧建立和恢复、跳转等开销程序运行速度更快。3.内联函数是函数编译器会对参数类型和返回值进行严格检查避免了宏替换中常见的逻辑漏洞。4.inline只是给编译器提个建议。编译器会根据函数的大小、复杂度以及自身的优化策略来决定是否真正内联函数体包含复杂的while、for循环、switch语句或递归调用编译器通常会拒绝内联。5.inline不建议定义与声明分离到两个文件分离会导致错误通常定义在.h文件中。例子#include iostream using namespace std; // 将宏改为内联函数 int square(int x) { return x * x; } int main() { int a 5; // 此时调用 square(a) int result square(a); //相当与a)*(a),即6*6 std::cout result result std::endl; // 输出 36 std::cout a a std::endl; // 输出 6 return 0; }没有inline时我们点击F10然后单击右键转到反汇编如图我们调用函数都是要根据函数地址进行跳转的但是加上inline时如图内联后编译器会把函数体直接展开到调用位置。缺点1.如果一个内联函数比较大且在程序中被调用了成百上千次最终编译出来的可执行文件体积会显著增大。2.如果函数太复杂有循环、递归、或是代码量太多编译器会为了保护程序性能而自动取消内联。例如inline int square(int x) { x; x; x; x; x; x; x; x; x; return x * x; }如图所以inline函数应用于函数调用次数较少函数内部不复杂函数体较小的情况下一篇为类与对象上

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价