赞
踩
暂时告别C语言,我们走进C++。对于有C语言基础,初学C++的我们来说,在正式学习C++的主体内容之前,我们需要先有一个过渡,本文中会总结过渡需要了解的零散知识,主要是语法。
C++兼容C语⾔绝⼤多数的语法,所以C语⾔实现的helloworld依旧可以运⾏,C++中需要把定义⽂件 代码后缀改为.cpp,vs编译器看到是.cpp就会调⽤C++编译器编译,linux下要⽤g++编译,不再是gcc。
// test.cpp
#include<stdio.h>
int main()
{
printf("hello world\n");
return 0;
}
然而C++有⼀套⾃⼰的输⼊输出,严格说C++版本的helloworld应该是这样写的。
// test.cpp
// 这⾥的std cout等看不懂,下面会依次讲解
#include<iostream>
using namespace std;
int main()
{
cout << "hello world\n" << endl;
return 0;
}
这个东西的出现是为了解决C语言中的一些问题。比如:
这是一段C语言代码,问题出在我们包含的stdlib.h这个头文件,我们知道头文件在预处理阶段会展开,也就是会将头文件拷贝过来,而在stdlib.h里有rand函数的定义,所以就与我们定义的变量命名冲突了。
也有可能是我们在与其他人协作时,代码合并后出现了很多冲突。可以说冲突就是C语言的一大问题。
所以我们在C++中有命名空间。
#include<stdio.h> #include<stdlib.h> //域 namespace oct { //命名空间中可以定义变量、函数和类型 int rand = 10; }//注意这里是没有分号的 int main() { //编译报错:重定义 printf("%d\n", rand);//此时rand已经被封起来了,这里的rand访问的是函数rand //这里如果真的想打印rand,改为%p更好 return 0; }
可以看出上面namespace定义中所说的,这个域跟全局域各自独立。
int a = 0;
int main()
{
int a = 1;
printf("%d\n", a);
return 0;
}
在这段代码中,全局和局部都有个a,那么我们打印时访问的是哪个a呢?答案是局部的,这是由于就近原则:先在局部找再去全局找。
这时候我们就需要用到**“域作用限定符”**,也就是两个冒号。
冒号左边不给东西,就默认去全局域查找了。
在域作用限定符左侧给上命名空间的名字,也就是告诉编译器直接去这个命名空间中查找。
namespace oct { //命名空间中可以定义变量、函数和类型 int rand = 10; int Add(int left, int right) { return left + right; } struct Node { struct Node* next; int val; }; }
注意,在命名空间中定义结构,我们要访问时的使用方式是这样的:
struct oct::Node p1;
namespace的前三条定义我们就看完了,现在再看三个定义:
在一个公司中我们可能有多个项目组,不同组用不同的命名空间,就不会冲突了。但是对于在同一个项目组的不同人来说,各自在各自的电脑上写,然后要合代码,比如用git提交代码,合起来可能就冲突了。也就是说在一个命名空间内部的冲突。
namespace oct { namespace october { int rand = 1; int Add(int left, int right) { return left + right; } } namespace octopus { int rand = 2; int Add(int left, int right) { return (left + right)*10; } } }
当然还可以再往下嵌套。
那么嵌套的命名空间里的成员如何访问?
同一个命名空间我们可以在同一个文件中定义多份,也可以在多个文件中定义多份。
比如我们现在有栈、队列、链表的文件,将它们都封装在namespace oct中,逻辑上会将它们合并,而不是冲突。否则我们一个文件一个命名空间,得定义很多的命名空间,显然不合适。
比如我们现在在全局定义一个静态的栈,再在命名空间定义动态的栈,在使用时我们就可以通过域的隔离分别使用,不会冲突。
标准库也怕和别人冲突。
C++标准库也有很多个各自的文件,都用namespace std合并封起来。
命名空间的使用有三种方式。
编译查找⼀个变量的声明/定义时,默认只会在局部或者全局查找,不会到命名空间里面去查找。所以下面程序会编译报错。所以我们要使⽤命名空间中定义的变量/函数,有三种⽅式:
上面我们使用的方式是指定命名空间访问。
而每次都指定命名空间还是挺麻烦的。
现在可以看到,我们就可以不用指定,直接使用命名空间里的a和b了。而这种写法是命名空间全展开。
但是展开整个命名空间的风险是很大的。只适合我们平时写日常小练习程序。
所以我们还有一种折中的方式:把命名空间中的某个成员展开。
假如我们要频繁使用a,且a不存在冲突风险:
所以一般在项目中我们选择将前两种方法结合使用。
只有内存里才有整型、浮点型这样的概念,在其他的地方比如文件、网络都是字符流的概念。
整型有原码、反码、补码,浮点数也有自己的存储规则,这都是内存里的存储。内存中有这样的概念是为了方便运算。
不管我们用cout还是printf,整型其实都是转换成字符。比如1234被转换为4个字符然后再输出。-1234被转换成5个字符再输出。任何类型都要转换为字符再输出到控制台。
#include<iostream>
using namespace std;//一般选择展开,更方便
int main()
{
int i = 1234;
int j = -1234;
cout << i << endl;
}
我们的插入可以一次写很多个连一起。
#include<iostream>
using namespace std;
int main()
{
int a = 0;
double b = 0.1;
char c = 'x';
cout << a << " " << b << " " << c << endl;
return 0;
}
还可以这样写:
cout << a << " " << b << " " << c << "\n";
在C++中用"\n"也可以换行。
而这样写就不行:
cout << a " " ;
cout也就是流插入其实是函数调用,只能有一个参数。<<是二元操作符,只能有两个操作数。
可以看到,C语言需要指定类型,比较麻烦,而C++却可以自动识别。
我们再来试试输入(流提取)。
从控制台中提取然后转化成对应的类型。
可以看到可以一次输入多个。
endl比较复杂,是一个函数。它的底层不是一个换行符。其实cout和cin也很复杂。这些都需要日后学得更深入再具体了解。
IO流涉及类和对象,运算符重载、继承等很多面向对象的知识,这些知识我们日后讲解。
cout/cin/endl等都属于C++标准库,C++标准库都放在⼀个叫std(standard)的命名空间中,所以要通过命名空间的使⽤⽅式去⽤他们。
这⾥我们没有包含<stdio.h>,也可以使⽤printf和scanf,在包含间接包含了。vs系列编译器是这样的,其他编译器可能会报错。
关于C语言中的控制精度:
可以看到我们的C++是默认保留小数点后5位。也可以人为控制,但是不建议了解。在我们想要控制时,可以用回printf。
在用scanf时我们需要取地址,因为scanf是个函数,[形参的改变不会影响实参,所以我们要取地址。]而在C++中我们就无需取地址。
再次总结,io流函数的定义在iostream头文件里的std命名空间里,可以自动识别类型,可以一行多个输入输出。
#include<iostream>
using namespace std;
int main()
{
// 在io需求⽐较⾼的地⽅,如部分⼤量输⼊的竞赛题中,加上以下3⾏代码
// 可以提⾼C++IO效率
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
return 0;
}
C++的IO流由于各种各样的原因比如要兼容C语言,而这种兼容会付出一些代价。IO输入输出是带缓冲区的,各自有各自的缓冲区,要兼容就得去关注对应的缓冲区。为了提高性能,除了换用printf、scanf,还可以选择把这三句代码加上。
• 缺省参数是声明或定义函数时为函数的参数指定⼀个缺省值。在调用该函数时,如果没有指定实参则采⽤该形参的缺省值,否则使用指定的实参,缺省参数分为全****缺省和半****缺省参数。(有些地方把缺省参数也叫默认参数)
• 全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值。C++规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。
• 带缺省参数的函数调⽤,C++规定必须从左到右依次给实参,不能跳跃给实参。
• 函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值。
#include <iostream>
using namespace std;
void Func(int a = 0)//缺省参数
{
cout << a << endl;
}
int main()
{
Func(); // 没有传参时,使⽤参数的默认值
Func(10); // 传参时,使⽤指定的实参
return 0;
}
其实就是如果有传就用传的,没有就用默认的值(缺省值)。
#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);//全传 Fun1(,2,);//这样传不可以 Func2();//不能一个都不传 Func2(100);//传一个 Func2(100, 200);//传两个 Func2(100, 200, 300);//全传 return 0; }
注意半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。
之前我们在写栈的插入数据时,如果后续要插入很多数据我们就要多次扩容,效率低下,而如果初始化时一把开好,就可以避免扩容。我们可以使用缺省参数:
(test.cpp中)
#include<iostream> using namespace std; #include"Stack.h" int main() { // 确定知道要插入1000个数据,初始化时一把开好,避免扩容 moon::ST s2; moon::STInit(&s2, 1000); for (size_t i = 0; i < 1000; i++) { moon::STPush(&s2, i); } return 0; }
(stack.h中)
void STInit(ST* ps, int n = 4);
(stack.cpp中)
这样我们就一把开好,不需要扩容,效率就得到了提高。在我们不知道多大时,不传参数,默认就为缺省参数。
缺省参数是很好用的,日后就知道了。
C++⽀持在同⼀作用域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者类型不同。这样C++函数调用就表现出了多态行为,使用更灵活。C语言是不⽀持同⼀作⽤域中出现同名函数的。
这个东西非常有用。可以说是C语言的一大缺陷。
#include<iostream> using namespace std; // 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; } int main() { Add(1,2); Add(1.1,2.2); return 0; }
以上代码,C语言是无法做到的。这种情况能同名显然是更好的。
再如
void Swap(int* px,int* py)
{}
void Swap(double* px, double* py)
{}
交换函数,如果能够同名,显然是非常好。
// 2、参数个数不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
// 3、参数类型顺序不同(本质也是类型不同)
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;
}
// 下⾯两个函数构成重载,因为参数个数不同
// f()但是调用时,会报错,存在歧义,编译器不知道调⽤谁
void f1()
{
cout << "f()" << endl;
}
void f1(int a = 10)
{
cout << "f(int a)" << endl;
}
//返回值不同不能作为重载条件,因为调⽤时也⽆法区分
void fxx()
{}
int fxx()
{
return 0;
}
这也是C++中非常好用的一个东西。
引⽤不是新定义⼀个变量,⽽是给已存在变量取了⼀个别名,编译器不会为引⽤变量开辟内存空间, 它和它引⽤的变量共⽤同⼀块内存空间。⽐如:⽔壶传中李逵,宋江叫"铁⽜",江湖上⼈称"黑旋风";林冲,外号豹⼦头;
类型&引用别名=引用对象;
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; }
无论叫a b c还是d,都是同一块空间的名字。给其中一个名字加1,其他的名字的值也都加1。
以前我们要交换两个变量的值,得使用传地址的方式。
我们现在使用引用,就无需传地址也能交换两个变量的值。
理解这个我们不要去想底层(底层也是指针),在语法层面就是取别名。
也就是rx是x的别名,ry是y的别名。改变别名就能改变本身。这样并没有为形参开辟新的空间。
我们可能会见到以下的代码:
void STInit(ST& rs, int n = 4)
{
rs.a = (STDataType*)malloc(n * sizeof(STDataType));
rs.top = 0;
rs.capacity = n;
}
这里就用别名来替换了指针。
再或者这样:
// 栈顶 void STPush(ST& rs, STDataType x) { //assert(ps); // 满了, 扩容 if (rs.top == rs.capacity) { printf("扩容\n"); int newcapacity = rs.capacity == 0 ? 4 : rs.capacity * 2; STDataType* tmp = (STDataType*)realloc(rs.a, newcapacity * sizeof(STDataType)); if (tmp == NULL) { perror("realloc fail"); return; } rs.a = tmp; rs.capacity = newcapacity; } rs.a[rs.top] = x; rs.top++; }
有了引用之后进入函数我们就不需要断言了。
还有原本下面的这个函数,我们是要用二级指针作为参数的,因为可能链表为空,push的这个就要成为头结点,那么plist就会改变,而如果参数为一级指针,phead改变无法改变plist,于是我们用二级指针。
void ListPushBack(LTNode*& phead, int x) { PNode newnode = (PNode)malloc(sizeof(LTNode)); newnode->val = x; newnode->next = NULL; if (phead == NULL) { phead = newnode; } else { //... } } int main() { LTNode* plist = NULL; ListPushBack(plist,1); return 0; }
而现在我们可以给指针取别名,就不用二级指针。所以我们现在改变指针phead,就是改变main中的指针plist。
typedef struct ListNode
{
int val;
struct ListNode* next;
}LTNode, *PNode;
//等价于
typedef struct ListNode* PNode;
PNode是指向LTNode类型的指针(结构体指针)。
所以上面函数也可以写成
void ListPushBack(PNode& phead, int x)
本文到此结束,但过渡内容没有结束,敬请期待下文=_=
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。