C++入门
首先第一点,C++中可以混用C语言中的语法。但是C语言是不兼容C++的。C++主要是为了改进C语言而创建的一门语言,就是有人用C语言用不爽了,改出来个C++。
命名空间
c语言中会有如下这样的问题:
那么C++为了解决这个问题就整出了一个命名空间。因为在日常作业中,我们为了使得代码的意思更加明确,我们创建的变量和函数有时候就会和c语言库里面创建的变量或者函数命名冲突。
或者是我和同事之间写的冲突,因为有可能大家负责不同板块,但是都要用到这个名字去定义一个函数或者变量,分开的时候没啥事,整合到一起就冲突了。
在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存 在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化, 以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
可以看到,当我把我创建出来的那个rand变量放进我创建的一个命名空间lin中,就不会有报错了。
定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{} 中即为命名空间的成员。
命名空间的名字尽量有意义些,一般最好不要和库里面的命名空间重复。不然还是有较大概率冲突的。
注意事项
-
在 C++ 里,命名空间的名字是可以重复的。当重复定义命名空间时,这些同名的命名空间实际上会合并成一个命名空间,各个定义中的成员会被整合在一起。
-
命名空间的名字是区分大小写的。
-
成员冲突:若在不同的同名命名空间定义中存在同名的成员,就会引发冲突,编译时会报错。
-
组织代码:利用同名命名空间合并的特性,可将一个大型的命名空间拆分成多个文件进行定义,以此来组织代码。例如,在不同的头文件里定义同一个命名空间的不同部分,最后将这些头文件包含到源文件中,就能够使用完整的命名空间成员。
-
-
命名空间可以嵌套定义。也就是在一个命名空间内部能够定义另一个命名空间。
命名空间的使用:
-
加命名空间名称及作用域限定符:
这样使用起来就比较麻烦了。 -
使用using将命名空间中某个成员引入:
- 使用using namespace 命名空间名称 引入:
"::"这个符号是域作用限定符。
注意事项:
- 在日常作业中,我们一般最好是不要使用第三种方式使用命名空间,尤其是C++官方库中的命名空间std。我们一般使用的方式是第二种,“使用using将命名空间中某个成员引入”。这样既可以方便使用,又可以避免一些不必要的麻烦。
- 毕竟使用命名空间把相关内容圈起来,肯定是不想你随意就展开的,这样命名空间的意义就不大了。当然了,如果只是平日的代码练习,直接展开也无碍,但是尽量形成良好的使用习惯。
输入(流提取)输出(流插入)
说明:
-
使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含< iostream >头文件 以及按命名空间使用方法使用std。
-
cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,他们都包含在包含< iostream >头文件中。
-
<<是流插入运算符,>>是流提取运算符。
-
使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动控制格式。 C++的输入输出可以自动识别变量类型。
-
实际上cout和cin分别是ostream和istream类型的对象,>>和<<也涉及运算符重载等知识, 这些知识我们我们后续才会学习,所以我们这里只是简单学习他们的使用。后面我们还有有 一个章节更深入的学习IO流用法及原理。
-
关于cout和cin还有很多更复杂的用法,比如控制浮点数输出精度,控制整形输出进制格式等 等。因为C++兼容C语言的用法,这些又用得不是很多,我们这里就不展开学习了。
缺省参数
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
给大家举个生活中的例子:你有个异性朋友,你很喜欢她/他。你这个异性朋友平时呢对你爱搭不理,因为他/她平时有很多比你更好的选项,她/他就去找别人玩。不过有时候呢,你这个异性朋友也有无聊的时候,没人找他/她玩,这个时候,她/他就想到你了,她/他也知道你喜欢她/他。她/他就会跟逗狗一样的逗你玩玩,或者让你陪她/他看个电影吃个饭,把钱给付了。然后就又忘记你,找别人去了。
懂?
诶,所以说做人不要当“缺省参数”。明白不?别TM当舔狗,当备胎。
不过“缺省参数”在C++中是条好狗,还是很好用的有时候。
缺省参数分类
- 全缺省参数
- 半缺省参数
注意:
- 半缺省参数必须从右往左依次来给出,不能间隔着给 ,必须是连续的给。
- 传参数的时候也是,必须连续的传,不能间隔,跳跃的传参数,而且必须从左往右传。
- 缺省参数不能在函数声明和定义中同时出现。缺省参数规定只能在函数声明时设置好,定义的时候就不用再设置缺省参数了。
- 如果声明与定义位置同时出现缺省参数,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值。就存在歧义了。
函数重载
函数重载就是可以有重名函数,但是重名函数之间的参数不同。
参数不同又有哪几种不同呢?
- 参数类型不同
- 参数类型不同中有一种是参数个数相同,但是参数类型的顺序不同
- 参数个数不同
看了这两幅图的解释之后,顺便说一下为什么返回值不能作为函数重载的依据,一样的,因为调用二义性。你只有返回值不同,鬼知道你到底要用哪个函数,对吧。
int func()
{return 0;
}double func()
{return 1.1;
}
来,你调用func()的时候,你说,你要调用哪个函数。这不就歧义了嘛,对吧。
这里还有个麻烦事,就是为什么C++支持函数重载,C语言不支持,这里我就不细说了,我把老师讲课的时候画的图给大家放出来,大家自己看看吧,我就不再讲了。
引用
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空 间,它和它引用的变量共用同一块内存空间。
这就像啥,像孙悟空似的,a是孙悟空,b是齐天大圣,c是弼马温,d是大圣,d是b的别名,总的来说b c d都指向着a:孙悟空。
注意:
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,再不能引用其他实体。也就是说,你不能既是a的别名,还是另一个别的变量的别名
void TestRef(){int a = 10;int b = 100;// int& ra; // 该条语句编译时会出错int& ra = a;// int& ra = b;//这个也是会报错的,不能引用多个实体int& rra = a;printf("%p %p %p\n", &a, &ra, &rra);
}
常引用
void TestConstRef(){const int a = 10;//int& ra = a; // 该语句编译时会出错,a为常量const int& ra = a;// int& b = 10; // 该语句编译时会出错,b为常量const int& b = 10;double d = 12.34;//int& rd = d; // 该语句编译时会出错,类型不同const int& rd = d;}
double d = 12.34;//int& rd = d; // 该语句编译时会出错,类型不同const int& rd = d;
这里大家可以会疑惑上面那个,为啥 //int& rd = d; // 该语句编译时会出错,类型不同
,但是加了一个const修饰就可以了呢?首先呢大家可以看到,这里是double类型隐式转换为int类型,这种类型转换之间都会产生一个临时变量,啥意思呢?意思就是隐式类型转换不是原来那个变量真的改变类型了,是用了一个临时变量存储了改变类型的原变量。用这里的例子就是,不是变量d真的变成了int类型,而是有一个临时变量存储了变量d变成int类型的值,然后赋值给变成新变量rd,这个临时变量是一个常量,所以引用的时候也要加一个const修饰。就像给常量10取别名一样。
// 权限不能放大
const int a = 10;
//int& b = a;//这样会报错
//大概意思就是:
//菩萨让孙悟空护唐僧取经,不能说你换个别名叫大圣了就不去护唐僧取经了
//或者说:小明在家里爸妈叫他有两个称呼,一个叫小明,一个是儿子
//现在爸妈不允许儿子吃饭,难道小明可以说:我叫小明,我可以吃饭。
//可以这样吗?不可以!
//这就类似权限的放大,你该干啥就要干啥,你不能干啥就不能干啥。
const int& b = a;// 权限可以缩小
int c = 20;
const int& d = c;
const int& e = 10;
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
下面这幅图是反汇编:
在C++中引用是无法完全替换指针的,指针和引用更多是相辅相成,引用是优化了一些原来C语言使用指针麻烦的地方。
引用和指针的不同点:
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何 一个同类型实体。比如指针a本来是变量1的地址,但是后面你想让指针a变成变量2的地址,这是可以的。但是引用就不行,引用一旦确定,就不能再改变它引用的实体,也就是说引用无法改变指向,所以在一些需要改变指向的功能中,就只能使用指针。
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32 位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
使用场景
引用一般两个使用场景,一个是:做参数,一个是:做返回值。
两种场景使用引用之后的主要效果都是:提高程序运行效率。
做参数的时候:(我们以传值和传引用做比较)
- 传值:在调用函数时,会把实参的值复制一份给形参。函数内部操作的是这个复制的值,而并非实参本身。所以,函数内部对形参的修改不会影响到实参。
- 传引用:调用函数时,传递给形参的是实参的引用,也就是实参的内存地址。函数内部对形参的操作实际上就是对实参本身进行操作,因此函数内部对形参的修改会影响到实参。
性能开销
- 传值:由于需要复制实参的值,当实参是较大的对象(如大型结构体、类对象)时,复制操作会消耗较多的时间和内存。
- 传引用:只需要传递对象的引用(内存地址),无需复制对象本身,所以在处理大型对象时,传引用的性能开销通常比传值小。
数据安全性
- 传值:函数内部无法修改实参的值,因此实参的数据在函数调用过程中是安全的。
- 传引用:函数内部可以直接修改实参的值,这可能会导致意外的数据修改。若不希望函数修改实参的值,可以使用
const
引用。示例如下:
#include <iostream>// 使用 const 引用,防止函数内部修改实参
void printValue(const int& num) {std::cout << "num 的值: " << num << std::endl;// num = 20; // 编译错误,不能修改 const 引用的值
}int main() {int num = 10;printValue(num);return 0;
}
引用做返回值的时候
在 C++ 中,使用引用作为函数的返回值有特定的条件、会产生相应的效果,同时也存在一些潜在的隐患。
使用引用做返回值的条件
1. 返回的对象必须在函数外部仍然有效
当函数返回一个引用时,这个引用所指向的对象必须在函数调用结束后仍然存在于内存中。如果返回的是函数内部的局部对象的引用,会导致未定义行为,因为局部对象在函数结束时会被销毁。因此,通常可以返回以下几种类型的引用:
- 全局变量或静态变量的引用:全局变量和静态变量的生命周期是整个程序运行期间,函数返回它们的引用是安全的。
#include <iostream>// 全局变量
int globalVar = 10;// 返回全局变量的引用
int& getGlobalVar() {return globalVar;
}// 静态变量
int& getStaticVar() {static int staticVar = 20;return staticVar;
}int main() {int& ref1 = getGlobalVar();int& ref2 = getStaticVar();std::cout << "Global var: " << ref1 << std::endl;std::cout << "Static var: " << ref2 << std::endl;return 0;
}
- 作为参数传入的对象的引用:如果函数接受一个对象的引用作为参数,那么可以安全地返回这个引用。
#include <iostream>int& modifyValue(int& num) {num *= 2;return num;
}int main() {int value = 5;int& result = modifyValue(value);std::cout << "Modified value: " << result << std::endl;return 0;
}
- 对象成员的引用:如果函数是类的成员函数,并且返回类对象的成员的引用,只要对象本身在函数调用结束后仍然有效,这种返回方式就是安全的。
#include <iostream>class MyClass {
public:int data;int& getData() {return data;}
};int main() {MyClass obj;obj.data = 10;int& ref = obj.getData();std::cout << "Data: " << ref << std::endl;return 0;
}
2. 函数的返回类型必须是引用类型
函数的返回类型需要明确指定为引用类型,即在类型后面加上 &
。例如:
int& func(); // 返回 int 类型的引用
使用引用做返回值的效果
1. 避免复制开销
返回引用可以避免对返回对象进行复制,特别是对于大型对象,这样可以提高程序的性能。例如:
#include <iostream>
#include <vector>// 返回向量的引用
std::vector<int>& getVector(std::vector<int>& vec) {return vec;
}int main() {std::vector<int> myVector = {1, 2, 3};std::vector<int>& result = getVector(myVector);// 没有进行复制操作,result 直接引用 myVectorreturn 0;
}
2. 可以作为左值使用
返回引用的函数可以作为左值,即可以出现在赋值语句的左边。这使得函数调用可以直接修改所引用的对象。
#include <iostream>int& getValue(int& num) {return num;
}int main() {int value = 5;getValue(value) = 10; // 函数调用作为左值std::cout << "Value: " << value << std::endl;return 0;
}
使用引用做返回值的隐患
1. 悬空引用
如果返回的引用指向的对象在函数调用结束后被销毁,就会产生悬空引用。访问悬空引用会导致未定义行为,可能会使程序崩溃或产生不可预测的结果。
#include <iostream>// 错误示例:返回局部变量的引用
int& getLocalValue() {int local = 10;return local; // 局部变量在函数结束时被销毁
}int main() {int& ref = getLocalValue();std::cout << ref << std::endl; // 悬空引用,未定义行为return 0;
}
2. 数据修改的风险
由于返回的引用可以直接修改所引用的对象,可能会导致意外的数据修改,特别是在多线程环境下,这种风险会更加明显。因此,在使用引用返回值时,需要谨慎考虑数据的安全性。
3. 代码可读性降低
过多使用引用返回值可能会使代码的可读性降低,因为引用的使用可能会让代码的逻辑变得复杂,尤其是在涉及多个函数调用和引用传递的情况下。开发者需要更加仔细地理解代码的执行流程和数据流向。
内联函数
那么内联函数主要是改变C语言中的啥呢?主要改变的是宏定义的问题。
宏定义
宏定义是在预处理阶段由预处理器处理的,它利用 #define
指令把一个标识符定义为一个字符串。在编译代码之前,预处理器会将代码里所有该标识符替换成对应的字符串。
语法
#define 宏名 替换文本
示例
#include <iostream>
// 定义一个简单的宏
#define PI 3.14159
// 定义一个带参数的宏
#define ADD(a, b) ((a) + (b))int main() {double radius = 5.0;double area = PI * radius * radius;std::cout << "圆的面积: " << area << std::endl;int x = 3, y = 4;int sum = ADD(x, y);std::cout << "两数之和: " << sum << std::endl;return 0;
}
优点
- 简单灵活:宏定义非常简单,能快速定义常量或者进行简单的代码替换,无需考虑类型。
- 无函数调用开销:由于宏只是简单的文本替换,不会产生函数调用的开销,在一些简单计算场景下能提高效率。
缺点
- 缺乏类型检查:宏只是简单的文本替换,预处理器不会对其进行类型检查,容易引发错误。
- 可能导致代码膨胀:如果宏在代码中被大量使用,会使代码体积增大,因为每次使用宏都会进行文本替换。
- 存在副作用:带参数的宏可能会产生副作用,例如宏参数可能会被多次求值。
- 不能调试
- 容易出错:在一些涉及运算符优先级的问题上,有时候你少一个括号,多一个括号就会有不同的后果。
内联函数
内联函数是一种特殊的函数,在编译时,编译器会尝试把函数调用处用函数体替换,以此避免函数调用的开销。
语法
inline 返回类型 函数名(参数列表) {// 函数体
}
//内联函数其实就是原本的函数多加一个inline修饰
示例
#include <iostream>
// 定义一个内联函数
inline int add(int a, int b) {return a + b;
}int main() {int x = 3, y = 4;int sum = add(x, y);std::cout << "两数之和: " << sum << std::endl;return 0;
}
优点
- 类型安全:内联函数是真正的函数,会进行类型检查,能减少因类型不匹配导致的错误。
- 避免代码膨胀:虽然内联函数会在调用处展开,但编译器会根据具体情况决定是否真正内联,能避免不必要的代码膨胀。一般函数内容如果超过10行就不会展开了。
- 代码可维护性高:内联函数和普通函数一样,有明确的函数定义和作用域,便于代码的维护和调试。
缺点
- 编译器决策:是否真正内联由编译器决定,即使使用了
inline
关键字,编译器也可能不进行内联。 - 不适合复杂函数:如果函数体比较复杂,内联可能会导致代码体积过大,反而降低性能。
宏定义和内联函数的区别
- 处理阶段不同:宏定义在预处理阶段处理,只是简单的文本替换;内联函数在编译阶段处理,由编译器决定是否内联。
- 类型检查:宏定义没有类型检查,内联函数有类型检查,更加安全。
- 代码膨胀:宏定义容易导致代码膨胀,内联函数由编译器控制,能避免不必要的代码膨胀。
- 调试难度:宏定义调试比较困难,因为预处理器替换后的代码可能和原始代码差异较大;内联函数和普通函数一样,调试相对容易。
下面这幅图就是假设函数内容是100行的时候,展开和不展开的时候反汇编的情况:(不严谨的)
这里补充一下,如果平时是分文件编写,那么内联函数声明和定义是不能分离的,也就是说不能你在头文件声明之后再去.c文件中定义,这里大家就在.h头文件里直接声明加定义写好就行了。因为虽说是内联函数,但是内联函数在编译链接的时候是直接展开的,不严谨的可以说他没有函数地址的,你要是声明和定义分离,到时候编译链接就会找不到你写的那个函数,明白嘛。
auto关键字
【注意】
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
auto的使用细则
- auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
- 在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
void TestAuto(){auto a = 1, b = 2; auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同}
auto不能推导的场景
- auto不能作为函数的参数
- auto不能直接用来声明数组
这里没有为啥,这主要是C++祖师爷的规定。然后这个auto的作用其实就是当一个变量的类型名很长的时候可以不用写嘛,但是目前我们还没有接触到很长了,等到后面学了之后就会慢慢接触到了。
基于范围的for循环
这里注意使用这个范围for,数组大小一定得是确定了的。
指针空值nullptr
NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
void TestPtr() {
int* p1 = NULL;
int* p2 = 0;
// ……
}


NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:
所以以后定义空指针换成nullptr就ok。