文章目录
- C++入门篇2
- 1、函数重载
- 1.1、函数重载概念
- 1.2、 C++支持函数重载的原理 -- 名字修饰(name Mangling)
- 2、引用
- 2.1、引用概念
- 2.2、引用特性
- 2.3、常引用
- 2.4、使用场景
- 2.5、传值、传引用效率比较
- 2.6、引用和指针的区别
- 3、内联函数
- 3.1、内联函数概念
- 3.2、内联函数特性
- 4、auto关键字(C++11)
- 4.1、auto简介
- 4.2、auto的使用
- 4.3、auto不能使用的场景
- 5、基于范围的for循环(C++11)
- 5.1、范围for的语法
- 5.2、范围for的使用条件
- 6、指针空值nullptr(C++11)
C++入门篇2
1、函数重载
1.1、函数重载概念
-
函数重载是指在同一作用域内使用相同的函数名定义多个函数,这些函数的参数列表(参数的个数或类型)必须不同。函数重载是一种多态的表现形式,常见于面向对象的编程语言如C++和Java。
#include <iostream> using namespace std; int Add(int a, int b) { return a + b; } float Add(float a, int b) { return a + b; } double Add(double a, int b) { return a + b; } int Add(double a, double b) { return a + b; } //函数重载 int main() { cout << Add(1, 1) << endl; cout << Add(1.1f, 1) << endl; //1.1f数据类型为float cout << Add(1.1, 1) << endl;//1.1 默认数据类型为double cout << Add(1.1, 1.1) << endl; std::cout << "Hello, World!" << std::endl; return 0; }
1.2、 C++支持函数重载的原理 – 名字修饰(name Mangling)
为什么C++支持函数重载,而C语言不支持函数重载呢?
在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。
这里我们以gcc编译器和g++编译器来演示C语言和C++的链接规则(过程好演示一点)。
这里涉及到C语言链接的时候是根据符号表去根据函数名去找相应的函数,当一个文件开始运行时,符号表会收集所有的函数名,这里函数名不加修饰,由于是根据函数名来找函数,所以函数名不能重复,不然不知找哪个!
而C++也是有符号表的,但是这个符号表记录的函数名和C语言不一样,这里的函数名会加以修饰,修饰规则是:==_Z+函数长度
+函数名+类型首字母==。
采用C语言的编译后结果
结论:在linux下,采用gcc编译完成后,函数名字的修饰没有发生改变。
采用C++语言的编译后结果
结论:在linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中。
- 通过这里就理解了C语言没办法支持重载,因为同名函数没办法区分。而C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。
- 如果两个函数函数名和参数是一样的,返回值不同是不构成重载的,因为调用时编译器没办法区分。
2、引用
2.1、引用概念
-
引用不是新定义一个变量,而是给已存在变量取了一个别名。编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
-
语法:类型& 引用变量名(对象名) = 引用实体;
int main() { int a = 1; int &b = a; cout << &b << endl << &a << endl;//地址一样 return 0; }
注意:引用类型必须和引用实体是同种类型的。
2.2、引用特性
-
引用在定义时必须初始化。
-
一个变量可以有多个引用,这些引用可以指向同一个实体,也可以指向不同的实体。
-
引用一旦引用一个实体,就不能再引用其他实体。
int main() { int a = 1, b = 0; int& c = a; int& d = a; //int& c = b;//报错,一个变量只能引用一个变量 cout << &a << endl << &c << endl << &d << endl;//地址一样 return 0; }
2.3、常引用
-
除了普通的引用外,C++还提供了常引用。所谓常引用,就是一旦一个常引用被初始化指向一个对象后,它就不能被重新指向另一个对象。
-
引用类型必须和引用实体是同种类型的。但是仅仅是同种类型,还不能保证能够成功引用。如果用一个普通引用类型去引用其对应的非常量类型对象,那么编译器将会报错。我们不可以可以将一个安全的类型(const修饰的类型)交给一个不安全的类型(可被修改的类型)。对于一个非常量引用,它可以引用一个非常量对象,也可以引用一个常量对象。但是对于常量对象,我们不能改变它的值也不能改变它的类型(即权限不能放大,但是权限可以缩小)。
int main() { //权限不能放大 const int a = 10; //int &b = a;//报错 const int &b = a; const int &c = b; //c = 1;//报错 //权限可以缩小 int d = 20; const int &e = d; //可以给常数取别名 const int &f = 10; int i = 1; double j = i; //double &rj = i; const double &rj = i;//i先int提升为double,提升后会有一个常变量记录这个提升后的i(i的原始值不变),所以必须使用常量来引用 return 0; }
2.4、使用场景
-
做参数:引用的一个常见用途就是在函数中作为参数使用。在函数中,我们可以通过引用来传递参数,而不是直接传递参数的值。这样可以避免数据的复制,提高了效率。
void Swap(int &a, int &b) { int tmp = a; a = b; b = tmp; } int main() { int a = 1, b = 3; cout << a << " " << b << endl;//1 3 Swap(a, b); cout << a << " " << b << endl;//3 1 return 0; }
-
做返回值:与做参数类似,我们也可以使用引用来返回函数的结果。这样可以避免数据的复制,提高了效率。在返回值是大型对象时特别有用。
int &Add(int a, int b) { static int c = a + b;//只初始化一次 return c; } int main() { int &ret = Add(1, 2); cout << ret << endl;//3 Add(4, 5); cout << ret << endl;//3 return 0; }
- 思考一个问题,为什么第二次输出
ret
的值也是3
?因为在Add
函数里,变量c
是静态变量,第一次调用Add
函数的时候它存在静态区(堆区),第二次调用的时候static int c = a + b;
不会执行,因为静态变量c
只会被初始化一次,那么第二次返回的就是堆区的c
,也就还是3
。
- 思考一个问题,为什么第二次输出
2.5、传值、传引用效率比较
在C++中,传值和传引用都可以用来传递参数给函数,但它们在执行效率和内存使用上有一些不同。
传值是通过复制实参的值给形参,在函数内部,形参是实参的一个副本,改变形参的值不会影响实参。这种方式不会避免对大型对象的复制操作,缺点是会占用额外的内存来存储副本。
传引用是通过将实参的内存地址给形参,在函数内部,形参可以直接访问实参的内存地址。这种方式可以直接访问并修改实参,优点是可以避免对大型对象进行昂贵的复制操作。
在执行效率方面:
- 传值对于小型对象来说效率较高,因为复制操作相对较快。
- 传值对于大型对象来说效率较低,因为复制操作需要花费更多的时间和内存。
- 传引用对于小型对象和大型对象来说效率都较高,因为不需要需要使用额外的内存来存储形参。
在内存使用方面:
- 传值需要额外内存来存储副本,如果传递的对象很大,会导致内存占用增加。
- 传引用不需要额外内存来存储副本,但是需要使用指针来访问对象,可能会导致指针错误或者空指针问题。
在实际编程中,选择传值还是传引用取决于具体的情况。如果传递的对象较大或者需要修改对象,使用传引用可以提高效率;如果只是需要传递对象的值而不是修改对象,使用传值可能更加合适。
-
值和引用的作为函数参数类型的性能比较
#include <time.h> struct A{ int a[10000]; }; void TestFunc1(A a){} void TestFunc2(A& a){} void TestRefAndValue() { A a; // 以值作为函数参数 size_t begin1 = clock(); for (size_t i = 0; i < 10000; ++i) TestFunc1(a); size_t end1 = clock(); // 以引用作为函数参数 size_t begin2 = clock(); for (size_t i = 0; i < 10000; ++i) TestFunc2(a); size_t end2 = clock(); // 分别计算两个函数运行结束后的时间 cout << "TestFunc1(A)-time:" << end1 - begin1 << endl; cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl; } int main(){ TestRefAndValue();//传引用效率更高 return 0; }
-
值和引用的作为返回值类型的性能比较
#include <time.h> struct A{ int a[10000]; }; A a; // 值返回 A TestFunc1() { return a;} // 引用返回 A& TestFunc2(){ return a;} void TestReturnByRefOrValue() { // 以值作为函数的返回值类型 size_t begin1 = clock(); for (size_t i = 0; i < 100000; ++i) TestFunc1(); size_t end1 = clock(); // 以引用作为函数的返回值类型 size_t begin2 = clock(); for (size_t i = 0; i < 100000; ++i) TestFunc2(); size_t end2 = clock(); // 计算两个函数运算完成之后的时间 cout << "TestFunc1 time:" << end1 - begin1 << endl; cout << "TestFunc2 time:" << end2 - begin2 << endl; } int main(){ TestReturnByRefOrValue();//传引用效率更高 return 0; }
2.6、引用和指针的区别
传引用不需要对大型对象进行复制操作。因为在C++中,当一个大型对象被传递给函数时,使用引用参数可以提高参数传递的效率。引用并不产生对象的副本,也就是说,在参数传递时,对象不需要被复制。因此,通过引用,函数可以直接访问并修改实参的内存地址,而不需要像传值那样将实参的值复制给形参,也不需要像传指针那样需要使用额外的内存来存储指针。因此,传引用对于大型对象来说可以提高效率。
-
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
-
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。但是我们一般都认为引用是没开空间的!
int main() { int a = 10; int& ra = a; ra = 20; int* pa = &a; *pa = 20; return 0; }
-
通过反编译可以对比引用和指针:
-
-
引用和指针的不同点:
- 基本性质:指针是一个变量,其内部存储的是指向内存存储单元的地址,而引用是原变量的一个别名,它们在内存中占用的存储单元是同一个。
- 赋值行为:在对引用进行第一次赋值时,起的是绑定作用,即给引用赋予一个新的名字,它和原变量是同一个东西。此后对引用的赋值,将会改变绑定变量的值。而对于指针,每次赋值都是赋给它自身的地址。
- 初始化要求:引用在初始化时必须绑定一个已存在的变量,而指针则无需初始化。
- 空值情况:指针可以为空(NULL),但引用不能为空。
- 可修改性:指针的值在初始化后可以修改,而引用在初始化后就不能修改了,即指针一个变量只能引用一个变量。
- 级别限制:指针可以有2级或2级以上,但是引用只能有一级。
- 安全性:使用引用比使用指针更安全。例如:引用的生命周期与所绑定的对象的生命周期是相同的,因此无需担心内存泄漏问题。
- 内存占用:在大多数情况下,指针所占内存为4字节,Linux下是8字节,而64位则不确定,引用所占内存与原变量所占内存大小相同。
总结起来,指针和引用虽然都是C++中重要的概念,但在使用上、功能上有明显的差异。
3、内联函数
3.1、内联函数概念
-
内联函数是指用inline关键字修饰的函数,或者在类体内定义的成员函数。
-
内联函数在编译时,会被嵌入到每一个调用处,而不是在调用时发生控制转移。这使得内联函数可以消除函数调用时的时间开销,通常用于频繁执行的函数。
如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用。
如下图:
3.2、内联函数特性
-
需要注意的是,递归函数不能被定义为内联函数。此外,内联函数一般适合于不存在复杂的结构(如while和switch等)且只有1~5条语句的小函数。如果一个内联函数有多个执行路径(如在if-else结构中),编译器可能会把它视为普通函数。
-
另外,使用内联函数时要注意:内联函数只能先定义后使用;不能对内联函数进行异常的接口声明。即内联函数不能声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
// F.h #include <iostream> using namespace std; inline void f(int i);//声明 // F.cpp #include "F.h" void f(int i)//定义 { cout << i << endl; } // main.cpp #include "F.h" int main() { f(10); return 0; } // 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用
4、auto关键字(C++11)
4.1、auto简介
在C++中,
auto
关键字用于自动类型推断,即让编译器根据变量的使用上下文自动确定其类型。auto
在C++11及其后的版本中引入,用于简化代码并减少手动指定变量类型的需求。使用
auto
关键字可以使代码更简洁,并可以减少因手动指定类型而产生的错误。例如,假设有一个复杂的表达式或变量,手动指定其类型可能会很麻烦或容易出错。使用auto
可以让编译器自动推断出正确的类型。
-
auto可以自动识别类型。如下:
#include <iostream> int main() { int a = 1; auto b = &a; auto c = a; auto d = b; std::cout << typeid(a).name() << std::endl << typeid(b).name() << std::endl << typeid(c).name() << std::endl << typeid(d).name() << std::endl;// i Pi i Pi //i表示int ,Pi表示Pointer int,也就是int* return 0; }
-
但是auto不是用在这么简单的类型上,在后面我们学的越来越多,越来越深后,类型的长度也会有更长的。比如迭代器。
#include <string> #include <map> int main() { std::map<std::string, std::string> m{{"apple", "苹果"}, {"orange", "橙子"}, {"pear", "梨"}}; std::map<std::string, std::string>::iterator it = m.begin(); while (it != m.end()) { //.... } return 0; }
- 这里的变量
it
的类型是std::map<std::string, std::string>::iterator
,这里我们用auto关键字就会使得代码简单很多。
#include <string> #include <map> int main() { std::map<std::string, std::string> m{{"apple", "苹果"}, {"orange", "橙子"}, {"pear", "梨"}}; auto it = m.begin(); while (it != m.end()) { //.... } return 0; }
- 这里的变量
-
注意:使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
4.2、auto的使用
-
auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&。
int main() { int x = 10; auto a = &x; auto *b = &x; auto &c = x; cout << typeid(a).name() << endl; cout << typeid(b).name() << endl; cout << typeid(c).name() << endl; *a = 20; *b = 30; c = 40; return 0; }
-
在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。void TestAuto() { auto a = 1, b = 2; //auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同 }
4.3、auto不能使用的场景
-
auto不能作为函数参数
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导 void TestAuto(auto a) { //考虑一个问题,如果这个函数定义了,但是没调用,那么auto推导的类型是什么?所以肯定不行吧 }
-
auto不能用来声明数组
void TestAuto() { int a[] = {1, 2, 3}; //auto b[] = {4,5,6};//auto不能用来声明数组 }
-
为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
-
auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环(基于范围的for循环),还有lambda表达式等进行配合使用。
5、基于范围的for循环(C++11)
5.1、范围for的语法
-
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
即
for(用于迭代的变量:被迭代的范围){}
。//传统for void TestFor1() { int array[] = {1, 2, 3, 4, 5}; for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i) array[i] *= 2; for (int *p = array; p < array + sizeof(array) / sizeof(array[0]); ++p) cout << *p << endl; } //新型for void TestFor2() { int array[] = {1, 2, 3, 4, 5}; for (auto &e: array) e *= 2; for (auto e: array) cout << e << " "; return 0; }
注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。
5.2、范围for的使用条件
-
for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循迭代的范围。 -
注意:以下代码就有问题,因为for的范围不确定
void TestFor(int array[]) { for (auto &e: array) cout << e << endl; }
-
迭代的对象要实现++和==的操作,我们看到这里for循环里使用
auto &e
,在范围for循环中使用auto&
可以引用容器或数组中的元素,并对这些元素进行下一步操作。
6、指针空值nullptr(C++11)
-
简而言之
nullptr
就是用来解决NULL
的在某些方面的不好。-
NULL
有可能被定义为字面常量0
,也可能被定义为无类型指针(void*)0
的常量。void f(int) { cout << "f(int)" << endl; } void f(int *) { cout << "f(int*)" << endl; } int main() { f(0);//输出f(int) f(NULL);//输出f(int) f((int *) NULL);输出f(int*) return 0; }
-
但是
nullptr
就纯纯是无类型指针(void*)0
的常量。void f(int) { cout << "f(int)" << endl; } void f(int *) { cout << "f(int*)" << endl; } int main() { f(0);//输出f(int) f(nullptr);//输出f(int*) f((int *) NULL);输出f(int*) return 0; }
-
-
注意:
-
在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
-
在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
-
为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
-
OKOK,C++入门篇2就到这里。如果你对Linux和C++也感兴趣的话,可以看看我的主页哦。下面是我的github主页,里面记录了我的学习代码和leetcode的一些题的题解,有兴趣的可以看看。
Xpccccc的github主页