从本篇开始,往后将开始更新C++有关的文章,本篇作为对C++的一个铺垫。将会较为详细的讲解一些有关C++的基本知识,便于读者从C语言阶段晋升到C++阶段。以下是对C++的一些介绍:
C++是在C的基础上,容纳进去了面对对象的编程思想,并且增加了许多有用的库,以及编程范式等等。熟悉C语言之后,对C++学习有一定的帮助。所以本篇的主要目标为:
1.补充C语言语法的不足,以及C++是如何对C语言设计不合理的地方进行优化的;
2.为后序类和对象的文章打下基础。
以下为本篇的目录:
目录
1. C++关键字
在命名对象时,我们不能将对象名命名成关键字,C++中的关键字如下:
C++总计63个关键字,C语言32个只有32个关键字。
2. 命名空间
在C/C++中,变量、函数和后面将要学习的类都是会大量存在的,对于这些变量、函数和类的名称将都存在于全局作用域之中,这可能会导致很多同名的冲突。使用命名空间的目的就是对标识符的名称进行本地化,避免命名冲突或名字污染,namespace关键字的出现就是针对这些问题的。如下图:
当我们定义一个全局变量rand时,这时候报错:出现重定义。是因为,在 stdlib.h 头文件中存在 rand() 函数,所以存在命名冲突。
2.1 命名空间定义
定义命名空间,需要使用 namespace 关键字,后面跟命名空间的名字,然后接一对 {} 即可,在 {} 中,即为命名空间的成员,如下:
// JZhong为命名空间的名字,一般开发中使用项目名字做命名空间 namespace JZhong { int rand = 10; int Add(int a, int b) { return a + b; } struct Node{ int data; struct Node* next; }; } // 命名空间可以嵌套 namespace JQZhang { int rand = 15; int Sub(int a, int b) { return a - b; } // 嵌套命名空间 namespace GGJ { int a; int b; int Add(int left,int right){ return left + right; } } } // 通常在一个工程中允许存在多个相同名称的命名空间,编译器最后会将其合成同一个命名空间 namespace JQZhang { int Mul(int a, int b) { return a * b; } }
如上所示,对于命名空间的定义,有着三个特性:
1. 一般以开发项目名字作为命名空间;
2. 命名空间中可以在嵌套一个命名空间;
3. 相同名称的命名空间最后将会合并。
2.3 命名空间的使用
对于命名空间的使用存在三种方式:
1. 加命名空间名称及作用域限定符
2. 使用using将命名空间中的某个成员引入;
3. 使用using namespace 命名空间名称 引入。
先介绍一个符号:“ :: ”,这两个冒号的符号被称为作用域限定符,对于这个限定符使用方式如下:
int main() { //第一种方法,命名空间名称+作用域限定符 printf("%d\n", JZhong::rand); printf("%d\n", JQZhang::rand); printf("%d\n", JQZhang::GGJ::Add(3, 5)); return 0; }
//第二种方法,使用using将命名空间中的某个成员引入 using JQZhang::Sub; using JZhong::Node; using JZhong::Add; int main() { Node node; node.data = 10; node.next = nullptr; printf("%d\n", Add(6, 8)); printf("%d\n", Sub(6, 8)); return 0; }
// 第三种方法,使用using namespace 命名空间名称 引入 using namespace JQZhang; int main() { GGJ::a = 10, GGJ::b = 20; printf("%d\n", Mul(GGJ::a, GGJ::b)); printf("%d\n", Sub(6, 8)); return 0; }
3. Cpp中的输出与输入
对于Cpp中的输入与输出,我们一般采用的函数为:标准输出流对象 cout、标准输出流对象 cin。对于这两个的使用,必须包含 <iostream> 头文件以及按照命名空间使用方法使用std。(注:Cpp同时兼容 printf 和 scanf函数)。
#include <iostream> // std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中 using namespace std; int main() { // endl函数代表'\n' cout << "hello world!\n" << endl; int i = 0; // 对i进行输入 cin >> i; std::cout << "hello world" << i << std::endl; return 0; }
对输入输出的说明:
1. 使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含< iostream >头文件以及按命名空间使用方法使用std。
2. cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,他们都包含在包含<iostream >头文件中。
3. “<<” 流插入运算符,“>>” 流提取运算符。4. 使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动控制格式。C++的输入输出可以自动识别变量类型。
对
ps:关于cout和cin还有很多更复杂的用法,比如控制浮点数输出精度,控制整形输出进制格式等等。因为C++兼容C语言的用法,对于控制浮点数输出精度可使用 printf 函数。
std 命名空间的使用惯例:1. 在平时的使用中,可以将 std 直接展开,这样更方便。
2. using namespace std展开,标准库就全部暴露出来了,如果我们定义跟库重名的类型/对象/函数,就存在冲突问题。所以不太建议直接在项目中展开 std 命名空间,可以使用以上第二种方法,展开一些常用的函数。
4. 缺省参数
4.1 缺省参数概念
缺省参数是声明或定义函数时,为函数的参数指定一个缺省值。在调用该函数时。如果没有指定实参,则采用该形参的缺省值,否则使用指定的实参。
当不传参时,函数就调用原指定的参数;传参之后则使用传过去的参数。
4.2 缺省参数的分类
缺省参数一种存在两种缺省参数,其中分别为:全缺省参数、半缺省参数。
全缺省参数:
void Func(int a = 0, int b = 1, int c = 2) { cout << "a = " << a << endl; cout << "b = " << b << endl; cout << "c = " << c << endl; }
半缺省参数:
void Func(int a, int b = 1, int c = 2) { cout << "a = " << a << endl; cout << "b = " << b << endl; cout << "c = " << c << endl; }
4.3 缺省参数的注意事项
对于缺省函数的使用,存在着这样的一些注意事项:
1. 半缺省参数必须从右向左依次给出,不能存在间隔。如下图:
2. 缺省参数不能在函数声明和定义中同时出现,如下图所示:
如上图所示,这样的定义会出现重定义的报错,我们需要在函数定义中将缺省参数都去掉。
3. 缺省值必须是常量或者全局变量;
4. C语言不支持缺省参数的使用。
5. 函数重载
5.1 函数重载的概念
函数重载:函数的一种特殊情况,C++中允许在同一作用域中声明几个功能相似的同名函数,这些同名函数的形参列表(参数个数、类型、类型顺序)不同,常用来处理实现功能相似数据类型不同的问题。
注:只能是形参列表不同,若列表相同,但返回值不同,则会报错。 如下图:
对于重载函数一共有三种模式:
1. 参数类型不同:
// 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. 参数个数不同:
// 2.参数个数不同 void Func() { cout << "Func()" << endl; } void Func(int a) { cout << "Func(int a)" << endl; }
3. 参数类型的顺序不同:
// 3.参数顺序不同 void F(int a, char b) { cout << "void F(int a, char b)" << endl; } void F(char a, int b) { cout << "void F(char a, int b)" << endl; }
5.2 C++支持函数重载的原理--名字修饰
为什么在C++中支持函数重载,而C语言中不会支持函数重载呢?
主要是在链接阶段出现了问题。在C/C++中,一个程序要运行起来,需要经过以下几个阶段:预处理、编译、汇编、链接。若想详细了解可看这篇文章:编译与链接(C/C++)-CSDN博客,具体的过程如下图(无预处理过程):
在链接阶段时,对于函数的调用,我们需要使用函数名去找到函数的地址,然后将其链接在一起。但是在C语言中,存在两个同名的函数,C语言无法区分具体要调用哪个函数,所以会报错。但是在C++中,对于函数有着不一样的函数修饰规则,所以在C++中每一个函数的函数修饰都不一样,所以可以找到对应的函数。如下图:
对于不同的函数修饰方式不一样,所以可以找到对应函数。
6. 引用
6.1 引用概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共同使用同一块内存空间。(可以理解为周树人和鲁迅、林冲和豹子头的关系)
使用方法:类型& 引用变量名(对象名) = 引用实体。
6.2 引用的特性
1. 引用在定义时必须初始化;
2. 一个变量可以引用多个引用;
3. 引用一旦引用某个实体,就不能在引用其他实体(引用不可以改变指向)
int main() { int a = 10; int c = 20; //int& rc; 为初始化,会报错 int& ra = a; int& rra = ra; printf("%p\n", &a); printf("%p\n", &ra); printf("%p\n", &rra); return 0; }
6.3 常引用
对于引用的类型一定要与被引用的变量一样,如下:
int main() { const int a = 10; //int& ra = a; //错误的方式 const int& ra = a; int b = 10; //const int& rb = b; //错误的方式 int& rb = b; double d = 12.34; //int& rd = d; 错误的方式 return 0; }
6.4 使用场景
第一种:做参数,使用引用做参数可以解决使用指针的大多数情况,如下:
int Swap(int& left, int& right) { int tmp = left; left = right; right = tmp; }
对于这样的函数,我们就不需要对传入的参数进行取地址了。
第二种:做返回值,对于使用引用做函数返回值,我们需要注意的是返回的值不能是局部变量,因为局部变量一旦出函数,这个局部变量就变成了随机值,返回的类型可以是:静态变量,动态函数开辟的变量、全局变量等等。
int& Count() { static int n = 10; n++; // ... return n; }
6.5 传值、返回值与传引用、返回引用效率比较
以值作为参数或者返回类型,在传参和返回阶段,函数不会直接传递实参或将本身变量直接返回,而是传递实参或者返回变量的一份临时拷贝,因此用值作为参数或者返回类型效率是十分低下的,特别是当参数或者返回值特别大时。
反而,对于传引用和返回引用则不同,引用是对变量的一个别名,对于引用的传递和返回,其实是直接对其传参,或者返回,其中并没有拷贝。测试代码如下:
#include <time.h> struct A { int a[10000]; }; void TestFunc1(A a) {}; void TestFunc2(A& a) {}; A a; A Testfunc1() {return a; }; A& Testfunc2() {; return a; }; int main() { A a; // 分别调用以上函数10000次,看计算时间 // 以值作为函数参数 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(); // 以值作为返回值 size_t begin3 = clock(); for (size_t i = 0; i < 10000; ++i) Testfunc1(); size_t end3 = clock(); // 以引用作为返回值 size_t begin4 = clock(); for (size_t i = 0; i < 10000; ++i) Testfunc2(); size_t end4 = clock(); // 分别计算两个函数运行结束后的时间 cout << "传参:" << endl; cout << "TestFunc1(A)-time:" << end1 - begin1 << endl; cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl; // 分别计算两个函数运行结束后的时间 cout << "返回值:" << endl; cout << "Testfunc1()-time:" << end3 - begin3 << endl; cout << "Testfunc2()-time:" << end4 - begin4 << endl; return 0; }
计算结果如下图:
6.6 指针与引用的区别
对于传地址和传引用有着很多的相似处,但是其中还是存在一些区别,如下:
1. 引用必须将其初始化,而指针可以不用初始化;
2. 引用不可以改变指向,但是指针可以改变指向,这个区别是指针和引用在使用上的很大区别,比如当我们遍历链表的时候,就需要使用指针而不能使用引用;
3. 引用相对而言会更加的安全,引用很少存在野引用,而对于指针使用不当则容易出现野指针;
4. sizeof 计算指针,一般情况计算的是指针所占空间的大小、对于引用就是计算的是引用变量的大小。对于 ++ 的使用、解引用访问也存在区别;
5. 存在多级指针,但不存在多级引用;
6. 访问方式不同,指针需要显式解引用、而引用则由编译器自己处理。
对于引用——将变量取一个别名,指针——指向存储变量的地址,这两种类型的变量语法概念上不同,但是对于底层实现:引用是使用指针实现的,如下图:
如上图的汇编代码,引用部分和指针部分的汇编代码一致,说明引用的底层实现是使用指针实现的。
7. 内联函数
7.1 内联函数的概念
以 inline 修饰的函数被叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数可以提升程序运行的效率。
提升效率的原因:当我们需要常用到一些代码量较小的函数时,每一次建立的都需要建立新的栈帧,开辟的空间和花的时间较多,但是使用内联函数时,就可以在调用函数时,将其替换,节省时间和空间。如下图:
7.2 内联函数的特性
1. inline 函数是一种时间换空间的做法。通常只将代码量较小的函数使用 inline 修饰,若遇到代码量较大的代码,不适合使用内联函数将其展开,若代码量太大的代码多次展开会导致代码膨胀,使目标文件变大。
2. inline 对于编译器只是一个建议,不同的编译器关于 inline 实现的机制不同。有的编译器对于代码量较大并且使用 inline 修饰的代码,并不会将其展开。
3. inline 函数不建议声明和定义分离,分离会导致链接错误。因为 inline 被展开,就没有函数地址,链接时就找不到该函数。
使用内联函数的特性,其实就相当于C语言中的宏,在预编译阶段直接将函数展开,但是更建议使用内联函数。
宏的优点虽然为:增强代码的复用性、提高性能。但是也存在一些较为突出的缺点:不方便调试,导致代码可读性、可维护性差,还没有类型安全的检查。
而使用内联函数,就可以更好的抵消掉宏的副作用。同时,我们还可以使用 const enum 进行常量定义。
8. auto关键字---C++11
8.1 问题引出
随着程序越来越复杂,程序中用到的类型也越来越复杂,如:
1. 类型难于拼写;
2. 含义不明确导致容易出错。如下:
#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; }
如上代码,std::map<std::string, std::string>::iterator 只是一个类型,但这个类型太长了,特别容易写错。但是我们也可以使用 typedef 对类型进行改名,不过就算改名也只能改一个,还存在其他冗长的类型,还需要一一进行改名。但是使用 auto 就会很好的解决这个问题,如下:
8.2 auto简介与使用细节
在C++11版本中,auto 作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。auto 可以自动的推测变量的类型,从而将变量的类型确立。
如上图,对于定义的变量,auto 可以自动推断其变量类型。
注意:使用 auto 定义变量时必须进行初始化,在在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型
auto 使用细节:1. auto 与指针和引用结合使用时:用 auto 声明指针类型时,用 auto 和 auto* 没有任何区别,但用 auto 声明引用类型时则必须加 &:
2. 在同一行定义多个变量时:在同一行定义多个变量时,定义的变量必须是相同类型的,否则将会报错:
3. auto 不能用来声明数组以及作为函数的参数:
9. 基于范围的 for 循环
9.1 范围 for 的语法
在C++98的标准中,当我们要遍历一个数组时,需要使用如下方式:
但是在C++11标准中引入了基于范围的 for 循环,for 循环后的括号由冒号“ : ”将其分为两部分:第一部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。如下:
使用范围 for 循环的条件:for 循环迭代的范围必须是确定的,对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
当数组传入到一个函数中时,就不可以在使用范围 for 循环了,因为函数传入的参数仅仅只为数组的首地址,不含有数组的范围。
若要在使用范围 for 循环时,更改遍历的值,我们可以使用引用来进行操作,如下:
10. 指针空值 nullptr (C++11)
标签:函数,int,C++,命名,初见,引用,使用,Cpp From: https://blog.csdn.net/m0_74830524/article/details/136656132在传统的C语言的 <stddef.h> 中,我们可以看见如下的代码:
#ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif #endif
可以看到,在C语言中,NULL被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,如:
在起初的程序本意是想通过func(NULL)调用指针版本的func(int*)函数,但是由于NULL被定义为0,因此与程序的初衷相悖。所以这时候,我们就可以使用 nullptr 关键字,代表空指针。