【C++基础篇】——逐步了解C++
文章目录
【C++基础篇】——逐步了解C++前言一、C++的第一个程序二、命名空间1.namespace的价值2.namespace的定义3.命名空间的使用 三、C++的输入&输出四、缺省参数五、函数重载六、引用1.引用的概念和定义:2.引用的特性3.引用的使用4.const引用5. 引用和指针的关系?
前言
本篇笔者将从C++的第一个程序开始讲解C++的知识点,请看笔者细细道来~
一、C++的第一个程序
我们了解到C++的雏形是在C的基础上设计而来,即C++兼容C语言的绝大数语法。所以C语⾔实现的hello world依旧可以运⾏,C++中需要把定义⽂件代码后缀改为.cpp,vs编译器看到是.cpp就会调⽤C++编译器编译。
//test.cpp#include<stdio.h>int main(){ printf("hello world\n"); return 0;}//这是我们学的第一个C语言程序
那用C++版本的hello world是什么样的呢?我们往下看。
//test.cpp#include<iostream>using namespace std;int main(){ cout<<"hello world\n"<<endl; return 0;}
我们可以看到除了头文件有变化外,下方还多了一行我们看不懂的代码,它的输出语句也和C语言不一样。下来我们先来了解多出的那行代码。
二、命名空间
我们先来看一个代码#include <stdio.h>int rand = 10;int main(){printf("%d\n", rand);return 0;}
这个代码没问题吧,我们也可以正常打印出来。接着往下看。 #include <stdio.h>#include <stdlib.h>int rand = 10;int main(){printf("%d\n", rand);return 0;}
这里我们加了一个头文件,然后它就显示报错,可以看到rand显示重定义了。原因就是#include<stdlib.h>在预处理阶段会展开,它里面也有一个命名为rand的函数,所以就会命名冲突。 1.namespace的价值
在C/C++中,变量、函数以及后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。
在避免命名冲突或名字污染的方式上,C语言的解决方式非常麻烦,Bjarne Stroustrup(C++之父)深受这个问题的困扰,就引入了命名空间。
使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
2.namespace的定义
1.定义命名空间,需要使⽤到namespace关键字,后⾯跟命名空间的名字,然后接⼀对{}即可,{}中即为命名空间的成员。命名空间中可以定义变量/函数/类型等。
#include <stdio.h>#include <stdlib.h>namespace luo{// 命名空间中可以定义变量/函数/类型 int rand = 10;int Add(int left, int right){return left + right;}struct Node{struct Node* next;int val;};}int main(){printf("%d\n", rand);return 0;}
2.namespace本质是定义出⼀个域,这个域跟全局域各自独立,不同的域可以定义同名变量,所以下面的rand不再冲突了。
上面的rand的在默认情况下是去访问全局的,即头文件里的rand函数,是个函数指针,所以会报错。我们把%d改成%p去打印这个函数指针变量所存储的地址。
要是我们想要访问命名空间里的rand,我们就要指定一下域,像这样:
int main(){ printf("%p\n",rand);//访问全局域 //::是域作用限定符 printf("%d\n",luo::rand);//访问luo这个命名空间域里的rand}
访问命名空间里的函数和结构体也是指定域即可
int main(){ printf("%p\n",rand);//访问全局域 //::是域作用限定符 printf("%d\n",luo::rand);//访问luo这个命名空间域里的rand luo::Add(1,2); struct luo::Node node;//要注意的是:结构体的域是要放在结构体的名称前面!}
3.C++中域有函数局部域,全局域,命名空间域,类域;域影响的是编译时语法查找⼀个变量/函数/类型出处(声明或定义)的逻辑,所有有了域隔离,名字冲突就解决了。局部域和全局域除了会影响编译查找逻辑,还会影响变量的生命周期,命名空间域和类域不影响变量生命周期。
#include <stdio.h>#include <stdlib.h>int x = 0;//全局域namespace luo{int x = 1;//命名空间域}void func(){int x = 2;//局部域只能在自己的局部访问}int main(){int x = 3;//局部域printf("%d\n", x);//main这个局部域printf("%d\n", luo::x);//访问命名空间域printf("%d\n",::x);//访问全局域return 0;}
namespace只能定义在全局,当然他还可以嵌套定义。 //命名空间可以嵌套#include<stdio.h>namespace luo{namespace AA{int rand = 1;int Add(int left, int right){return left + right;}}namespace BB{int rand = 2;int Add(int left, int right){return (left + right) * 10;}}}int main(){printf("%d\n", luo::AA::rand);printf("%d\n", luo::BB::rand);printf("%d\n", luo::AA::Add(1, 2));printf("%d\n", luo::BB::Add(1, 2));return 0;}
5.项⽬工程中多文件中定义的同名namespace会认为是一个namespace,不会冲突。
//Stack.h#pragma once#include<stdio.h>#include<stdlib.h>#include<stdbool.h>#include<assert.h>namespace luo{typedef int STDataType;typedef struct Stack{STDataType* a;int top;int capacity;}ST;void STInit(ST* ps,int n);void STDestroy(ST* ps);void STPush(ST* ps, STDataType x);void STPop(ST* ps);STDataType STTop(ST* ps);int STSize(ST* ps);bool STEmpty(ST* ps);}//Stack.cpp#include"Stack.h"namespace luo{void STInit(ST* ps, int n){assert(ps);ps->a = (STDataType*)malloc(n * sizeof(STDataType));ps->top = 0;ps->capacity = n;}// 栈顶void STPush(ST* ps, STDataType x){assert(ps);// 满了, 扩容if (ps->top == ps->capacity){int newcapacity = ps->capacity == 0 ? 4 : ps->capacity* 2;printf("扩容:%d\n", newcapacity);STDataType* tmp = (STDataType*)realloc(ps->a,newcapacity * sizeof(STDataType));if (tmp == NULL){perror("realloc fail");return;}ps->a = tmp;ps->capacity = newcapacity;}ps->a[ps->top] = x;ps->top++;}}// test.cpp#include"Queue.h"#include"Stack.h"// 全局定义了⼀份单独的Stacktypedef struct Stack{int a[10];int top;}ST;void STInit(ST* ps) {}void STPush(ST* ps, int x) {}int main(){// 调⽤全局的ST st1;STInit(&st1);STPush(&st1, 1);STPush(&st1, 2);printf("%d\n", sizeof(st1));// 调⽤luo namespace的luo::ST st2;printf("%d\n", sizeof(st2));luo::STInit(&st2);luo::STPush(&st2, 1);luo::STPush(&st2, 2);return 0;}
6.C++标准库都放在⼀个叫std(standard)的命名空间中。(为了防止标准库和我们定义的namespace冲突)
3.命名空间的使用
编译查找⼀个变量的声明/定义时,默认只会在局部或者全局查找,不会到命名空间⾥⾯去查找。所以下⾯程序会编译报错。
所以我们要使用命名空间中定义的变量/函数,有三种方式:
1.指定命名空间访问,项⽬中推荐这种方式。
#include<stdio.h>namespace luo{int a = 0;int b = 1;}int main(){printf("%d\n",luo:: a);//指定命名空间访问return 0;}
2.using将命名空间中某个成员展开,项⽬中经常访问的不存在冲突的成员推荐这种⽅式。
#include<stdio.h>namespace luo{int a = 0;int b = 1;}using luo::a;// using将命名空间中某个成员展开int b = 3;int main(){printf("%d\n",a);printf("%d\n",b);luo::b++;printf("%d\n", luo::b);return 0;}
3.展开命名空间中全部成员,项⽬不推荐,冲突风险很大,日常小练习程序为了方便推荐使⽤。
#include<stdio.h>namespace luo{int a = 0;int b = 1;}using namespace luo;// 展开命名空间中全部成员int main(){printf("%d\n",b);return 0;}
三、C++的输入&输出
是 Input Output Stream 的缩写,是标准的输⼊、输出流库,定义了标准的输入、输出对象。
std::cin 是 iostream 类的对象,它主要⾯向窄字符(narrow characters (of type char))的标准输入流。
std::cout 是iostream 类的对象,它主要⾯向窄字符的标准输出流。
std::endl 是⼀个函数,流插⼊输出时,相当于插⼊⼀个换行字符加刷新缓冲区。
<<是流插⼊运算符,>>是流提取运算符。(C语⾔还⽤这两个运算符做位运算左移/右移)
使⽤C++输⼊输出更⽅便,不需要像printf/scanf输⼊输出时那样,需要⼿动指定格式,C++的输⼊输出可以⾃动识别变量类型(本质是通过函数重载实现的,这个以后会出),其实最重要的是C++的流能更好的⽀持⾃定义类型对象的输⼊输出。
IO流涉及类和对象,运算符重载、继承等很多⾯向对象的知识。(后续会出)
cout/cin/endl等都属于C++标准库,C++标准库都放在⼀个叫std(standard)的命名空间中,所以要通过命名空间的使⽤⽅式去⽤他们。
⼀般⽇常练习中我们可以using namespace std,实际项⽬开发中不建议using namespace std。
这⾥我们没有包含<stdio.h>,也可以使⽤printf 和scanf,在包含 < iostream>间接包含了。vs系列编译器是这样的,其他编译器可能会报错。
#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>//using namespace std;//using std::cout;//using std::endl;int main(){// <<流插入std::cout << "hello world\n";int i = 10;std::cout << i<<'\n' << "\n";double d = 1.1;std::cout << d << std::endl;std::cin >> i >> d;std::cout << i << " " << d << std::endl;scanf("%d%lf", &i, &d);printf("%d %.2lf\n", i, d);return 0;}
看到这里,大家应该可以理解C++的第一个程序的含义了。
四、缺省参数
C语言中没有缺省参数的概念,C++支持缺省参数。
1.缺省参数是声明或定义函数时为函数的参数指定⼀个缺省值。在调⽤该函数时,如果没有指定实参则采⽤该形参的缺省值,否则使用指定的实参,缺省参数分为全缺省和半缺省参数。(有些地方把缺省参数也叫默认参数)
#include <iostream>#include <assert.h>using namespace std;void Func(int a = 0){cout << a << endl;}int main(){Func(); // 没有传参时,使⽤参数的默认值Func(10); // 传参时,使⽤指定的实参return 0;}
2.全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值。C++规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。
#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;}
3.带缺省参数的函数调用,C++规定必须从左到右依次给实参,不能跳跃给实参。
Func1(,2,);//不支持Func1(,2,3);//不支持
4.函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值。
// Stack.h#include <iostream>#include <assert.h>using namespace std;typedef int STDataType;typedef struct Stack{STDataType* a;int top;int capacity;}ST;void STInit(ST* ps, int n = 4);// Stack.cpp#include"Stack.h"// 缺省参数不能声明和定义同时给void STInit(ST* ps, int n){assert(ps && n > 0);ps->a = (STDataType*)malloc(n * sizeof(STDataType));ps->top = 0;ps->capacity = n;}// test.cpp#include"Stack.h"int main(){ST s1;STInit(&s1);// 确定知道要插⼊1000个数据,初始化时⼀把开好,避免扩容ST s2;STInit(&s2, 1000);return 0;}
五、函数重载
函数重载是C++中的一项特性,它允许在同一作用域内声明具有相同名称但参数列表不同的函数。
函数名需相同。参数的个数、参数的顺序和参数的类型不同均可构成重载。在同一作用域。返回值类型不同不能构成重载,因为调用时也无法区分。#include<iostream>using namespace std;//参数类型不同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;}//参数个数不同void f(){cout << "f()" << endl;}void f(int a){cout << "f(int a)" << endl;}//参数类型顺序不同(本质还是类型不同)void f(int a, char b){cout << "f(int a,char b)" << endl;}void f(char b, int a){cout << "f(char b, int a)" << endl;}int main(){Add(10, 20);Add(10.1, 20.2); f(); f(10); f(10, 'a'); f('a', 10); return 0;}
六、引用
1.引用的概念和定义:
引⽤不是新定义⼀个变量,⽽是给已存在变量取了⼀个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共⽤同⼀块内存空间。
类型& 引⽤别名 = 引⽤对象;
C++中为了避免引⼊太多的运算符,会复⽤C语⾔的⼀些符号,⽐如前⾯的<< 和 >>,这⾥引⽤也和取地址使⽤了同⼀个符号&,⼤家注意使⽤⽅法⻆度区分就可以。
#include<iostream>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;}
2.引用的特性
引用在定义时必须初始化
一个变量可以有多个引用
引⽤一旦引⽤一个实体,再不能引⽤其他实体
3.引用的使用
1.引⽤在实践中主要是于引用传参和引⽤做返回值中减少拷贝提⾼效率和改变引用对象时同时改变被引用对象。
2.引⽤传参跟指针传参功能是类似的,引⽤传参相对更⽅便⼀些。
3.引⽤返回值的场景相对⽐较复杂。
4.引⽤和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。C++的引⽤跟其他语⾔的引⽤(如Java)是有很⼤的区别的,除了⽤法,最⼤的点,C++引⽤定义后不能改变指向,Java的引⽤可以改变指向。
5.⼀些主要⽤C代码实现版本数据结构教材中,使⽤C++引⽤替代指针传参,⽬的是简化程序,避开复杂的指针,但是很多同学没学过引⽤,导致⼀头雾⽔。
4.const引用
1.可以引⽤⼀个const对象,但是必须⽤const引⽤。const引⽤也可以引⽤普通对象,因为对象的访问权限在引⽤过程中可以缩⼩,但是不能放⼤。
int main(){const int a = 10;//int& ra = a;//引用对a的访问权限放大了,使得a可以修改;权限不能放大const int& ra = a;//正确写法int b = 7;const int& rb = b;//权限可以缩小//rb++;//rb自己的权限是只读不能写b++;const int x = 0;int y = x;//拷贝赋值const int a = 10;const int* p1 = &a;//int* p2 = p1;//权限不能放大int b = 25;int* p3 = &b;const int* p4 = p3;//权限可以缩小int* const p5 = &b;int* p6 = p5;//不存在权限放大,因为const修饰的是p4本身不是指向的内容return 0;}
2.在 C++ 中,当你想引用一个变量时,你可以使用引用(int&
这样的方式),但引用的对象必须是可以修改的变量。如果引用的对象是一个临时值或者常量,就会出现问题。
比如说,在下面的代码中:
int a = 5;int& rb = a * 3; // a * 3 是一个临时结果const int& rb = a * 3;
这里 a * 3
计算的结果会创建一个临时对象,而 rb
尝试引用这个临时对象。由于临时对象是“常”的,意味着你不能修改它,C++ 会报错。
再比如说:
double d = 12.34;int& rd = d; // d 是 double 类型,不能用 int& 引用const int& rd = d;
这里 d
是个 double
类型的变量,而你用 int&
去引用它,这也是不行的,因为 double
到 int
的转换会生成一个临时对象。
简单来说,如果你尝试用引用去引用那些只能读取而不能修改的临时值,或者类型不匹配的对象,C++ 会因为这些临时对象的常性而引发错误。这就要求你在这些情况下使用常量引用,如 const int&
,这样就允许引用临时对象而不会引发问题。
3.所谓临时对象就是编译器需要⼀个空间暂存表达式的求值结果时临时创建的⼀个未命名的对象,C++中把这个未命名对象叫做临时对象。
5. 引用和指针的关系?
C++中指针和引用就像两个性格迥异的亲兄弟,指针是哥哥,引用是弟弟,在实践中他们相辅相成,功能有重叠性,但是各有自己的特点,互相不可替代。
1.在语法概念上:引用是⼀个变量的取别名,不开空间;指针是存储⼀个变量地址,要开空间。
2.引用在定义时必须初始化;指针建议初始化,但是语法上不是必须的。
3.引用在初始化时引用⼀个对象后,就不能再引用其他对象;而指针可以在不断地改变指向对象。
4.引用可以直接访问指向对象,指针需要解引用才是访问指向对象。
5.sizeof中含义不同,引用结果为引用类型的大小;但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8byte)。
6.指针很容易出现空指针和野指针的问题;引用很少出现,引用使用起来相对更安全⼀些。
最后,本篇文章到此结束,感觉不错的友友们可以一键三连支持一下笔者,有任何问题欢迎在评论区留言哦~