从C到C++基础
文章目录
前言
c++的历史可以追溯到1979年,创始人Bjarne Stroustrup (c++祖师爷[doge]),他在实验室工作时候,感受到了现有语言(如c语言)在表达能力、可维护性和可扩展性方面的不足。于是乎,祖师爷决定着手扩展c语言,解决c再实际应用中的问题,随后,竟在c语言的基础上扩展出了c++编程语言。
本篇博文就将追随祖师爷的扩展之路,对比c语言,介绍c++的基础语法。
一、c++的"hello world"
#include<iostream>
using namespace std;
int main()
{
cout << "Hello world!";
return 0;
}
观察以上代码,我们发现,c++和c存在相似之处,如仍需用#include 来包含头文件,并且程序的框架仍然有主函数。cout << 类似于c中的printf函数。
至于这句
using namespace std;
就到了我们的第一个c++语法知识点:命名空间。
二、命名空间
在编写c代码的过程中,我们或许会遇到命名冲突的问题:
#include <stdio.h>
#include <stdlib.h>
int rand = 10; // 定义一个随机数rand
int main()
{
printf("%d", rand);
return 0;
}
以上代码中我们定义一个rand变量,并试图输出它。然后运行代码,却会发现报错了 r a n d : 重定义 ; 以前的定义是函数 rand:重定义; 以前的定义是函数 rand:重定义;以前的定义是函数
这是由于c语言自带头文件stdlib.h中也有一个函数命名为rand。若我们包含了<stdlib.h>,同时又使用了rand,就会报错。
为了解决命名冲突带来的困扰,祖师爷引入了命名空间
1.命名空间定义及基础使用
关键字:namespace
#include<stdio.h>
#include<stdlib.h>
// 1. 正常的命名空间定义
// bit是命名空间的名字,⼀般开发中是⽤项⽬名字做命名空间名。
namespace infi {
int rand = 10;
int Add(int x, int y)
{
return x = y;
}
}
int main()
{
// 这里默认访问 rand 函数的地址
printf("%p\n", rand);
// 这里访问的是命名空间中的rand变量
printf("%d\n", infi::rand);
return 0;
}
我们使用namespace 关键字来定义一个命名空间,需要使用其中的元素时需要使用 ”空间名“ + ”::“ + “变量名”。
2.一些特性
可嵌套:
#include <stdio.h>
#include <stdlib.h>
namespace A {
namespace zhangsan{
int age = 33;
}
namespace lisi {
int age = 23;
}
}
int main()
{
printf("%d\n", A::zhangsan::age);
printf("%d\n", A::lisi::age);
return 0;
}
如上述代码所示,比如某公司的两个程序员,张三和李四,他们共同开发一个项目A,共同使用项目A的命名空间。为了防止命名冲突,于是乎,他们就在公司的命名空间中定义了各自的命名空间。
到这里,有人就会问了:这样子不会让代码很难写吗,一个变量还要经过一层甚至多层命名空间。
对于这个问题:
1.对于开发大型项目而言,如果只是在使用变量时加上命名空间,就能解决命名冲突带来的运行错误和调试困难,那么,相对而言,这是很划算的(开发用的ide往往有大量的代码补全,让编码速度不会降低很多)
2.c++中,如果你确定这个变量一定不会冲突,那么也可以使用展开命名空间的方法省取”命名空间::“
同名合并:
#include <stdio.h>
#include <stdlib.h>
namespace A {
namespace zhangsan{
int age = 33;
}
namespace lisi {
int age = 23;
}
}
namespace A {
int rand = 10;
}
像这样,命名空间会直接合并。
当然,这个特性的应用场景肯定不是这样,因为这样不如我直接把 int rand = 10写在第一个A里面。
它的更优的场景是在多文件中,将同名空间自动合并
3.命名空间的使用
(1)指定命名空间访问
场景:项目推荐使用这种方式
#include<stdio.h>
namespace bit
{
int a = 0;
int b = 1;
}
int main()
{
// 编译报错:error C2065: “a”: 未声明的标识符
printf("%d\n", a);
return 0;
}
(2)using 将命名空间某个成员展开
项⽬中经常访问的不存在冲突的成员推荐这种⽅式。
#include<stdio.h>
namespace N
{
int a = 0;
int b = 1;
}
// int main()
// {
// // 编译报错:error C2065: “a”: 未声明的标识符
// printf("%d\n", a);
// return 0;
// }
using N::b; // 展开b
int main()
{
printf("%d\n", N::a);
printf("%d\n", b); // 展开了b就不用写命名空间+::
return 0;
}
(3)展开全部成员:
项目中冲突的风险很高,不推荐使用,可在日常小练习中使用
#include<stdio.h>
namespace N
{
int a = 0;
int b = 1;
}
using namespace N;
int main()
{
// a,b都无需加命名空间
printf("%d\n", a);
printf("%d\n", b);
return 0;
}
三、c++输入和输出
1.简介
- 注意,由于c++的输入输出用到大量的面向对象知识,这里只简单介绍,后续类和对象之后会详细介绍
- 是 Input Output Stream 的缩写,是标准的输⼊、输出流库,定义了标准的输⼊、输出对象。
- std::cin就类似与sanf, std::cout类似于prinf(),std::endl,可简单理解为换行。
- << 是流插入运算符,配合std::cout输出内容,>> 是流提取运算符,配合std::cin输入内容。
- cout, cin, endl以及后续的一系列c++标准库中的函数,变量都在std命名空间中(standard), 所以需要用命名空间的方法使用它们。
- ⼀般⽇常练习中我们可以using namespace std直接展开std命名空间,从而直接使用cin等。但是,实际项⽬开发中不建议using namespace std,因为会存在大量命名冲突
2.用法示例
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <stdio.h>
int main()
{
int a = 0;
double b = 0.1;
char c = 'x';
std::cout << a << " " << b << " " << c << std::endl;
std::cout << a << " " << b << " " << c << std::endl;
scanf("%d%lf", &a, &b);
printf("%d %lf\n", a, b);
// 可以⾃动识别变量的类型
std::cin >> a;
std::cin >> b >> c;
std::cout << a << std::endl;
std::cout << b << " " << c << std::endl;
return 0;
}
为了适应项目标准,我尽可能在平时练习采用std::的方式编写代码,供参考。
四、缺省参数
常用:
意义:缺省参数就是给函数的形参一个默认值,若调用函数时传了实参,则采用这个实参,否则直接采用默认值
-
全缺省是全部形参都缺省(都给默认值);半缺省则是给一部分缺省。c++规定,半缺省必须从右往左连续地缺省,不能跳跃着给缺省(默认值)。
-
调用函数时,必须从左往右一次传实参不能跳跃。这里跟上一点很好理解,右边末尾的几个缺省参数可传可不传参,而左边的几个无缺省的参数需要传参。
代码示例:
#include <iostream> // 这是错误的 //void print(int a = 0, int b, int c = 1) //{ // std::cout << a << ' ' << b << ' ' << c << std::endl; //} void print(int a, int b = 0, int c = 1) { std::cout << a << ' ' << b << ' ' << c << std::endl; } int main() { print(1); print(1, 10); print(1, 10, 20); // 这是错误的 /*print(1, , 20);*/ return 0; }
特殊:
函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值。
这句话的意思是指函数声明和定义分离时,只能在定义或声明的一个位置出现缺省,如下:
// 只在定义给缺省参数,不使用这种方法
void print(int a, int b, int c);
void print(int a, int b = 0, int c = 1)
{
std::cout << a << ' ' << b << ' ' << c << std::endl;
}
// 只在声明给缺省参数(规定)
void print(int a, int b = 0, int c = 1);
void print(int a, int b, int c)
{
std::cout << a << ' ' << b << ' ' << c << std::endl;
}
为什么规定在声明给缺省值呢?
答:在多文件中,声明一般写在在头文件内,我们包含头文件之后,编译器编译时只会编译头文件的内容(不编译.cpp文件中的函数实现),若此时只在定义处有缺省,那么编译器会认为这个函数无缺省参数,此时就达不到我们的目的了。
五、函数重载
定义和用法
c++支持函数同名,但是形参不同的情况出现,这种用法称为函数重载。(c语言不支持)
可以是形参的类型不同或个数不同
如下所示
#include <iostream>
void f1(int a, int b)
{
std::cout << "void f1(int a, int b)" << std::endl;
}
void f1(int a, int b, int c)
{
std::cout << "void f1(int a, int b, int c)" << std::endl;
}
void f1(double a, double b)
{
std::cout << "void f1(double a, double b)" << std::endl;
}
在调用时,只需要传入对应类型和数量的参数,编译器就会自动是识别调用对应的函数
注意
函数的返回值不同不构成重载
试想这种情况
void f1()
{
// ...
}
int f1()
{
return 0;
}
int main()
{
f1()
return 0;
}
因为函数有返回值时,我们也可以不接收返回值的调用。这样在主函数调用f1()就存在歧义,因此,c++中,返回值不同不构成重载。
六、引用(非常重要)
1.引用的定义和概念
引⽤不是新定义⼀个变量,⽽是给已存在变量取了⼀个别名,编译器不会为引⽤变量开辟内存空间,
用法:
类型& 引⽤别名 = 引⽤对象;
2.引用的特性
• 引⽤在定义时必须初始化
• ⼀个变量可以有多个引⽤
• 引⽤⼀旦引⽤⼀个实体,再不能引⽤其他实体
#include<iostream>
using namespace std;
int main()
{
int a = 10;
// 编译报错:“ra”: 必须初始化引⽤
//int& ra;
int& b = a;
int c = 20;
// 这⾥并⾮让b引⽤c,因为C++引⽤不能改变指向,
// 这⾥是⼀个赋值
b = c;
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
return 0;
}
3.引用的使用
• (1)引⽤在实践中主要是于 引⽤传参和引⽤做返回值 中减少拷⻉、提⾼效率和改变引⽤对象时同时改变被
引⽤对象。
•(2) 引⽤传参跟指针传参功能是类似的,引⽤传参相对更⽅便⼀些。
• (3)引⽤返回值的场景相对⽐较复杂,这里只简单提一下,后续类和对象会再次详细介绍
#include <iostream>
// 引用实现交换
void swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
// 指针实现交换
void swap(int* pa, int* pb)
{
int tmp = *pa;
*pa = *pb;
*pb = tmp;
}
int main()
{
int a, b;
a = 1, b = 0;
// 引用 实现交换
swap(a, b);
// 指针实现交换
swap(&a, &b);
return 0;
}
引用相对于指针,省去了取地址和解引用操作,在特定的场景下会更方便。
注意:
- 从更底层的角度(汇编),引用的底层仍然是指针的解引用,只不过c++的语法规定,让我们在编写代码时可以直接使用引用。
- 引用和取地址都是’&'操作符,注意从用法区分
4.const 引用
•(1) 可以引⽤⼀个const对象,但是必须⽤const引⽤。const引⽤也可以引⽤普通对象,因为对象的访问权限在引⽤过程中可以缩⼩,但是不能放⼤。
• (2)不需要注意的是类似 int& rb = a3; double d = 12.34; int& rd = d; 这样⼀些场景下a3的和结果保存在⼀个临时对象中, int& rd = d 也是类似,在类型转换中会产⽣临时对象存储中间值,这时,rb和rd引⽤的都是临时对象,⽽C++规定临时对象具有常性,所以这⾥就触发了权限放⼤,必须要⽤常引⽤才可以。
•(3) 所谓临时对象就是编译器需要⼀个空间暂存表达式的求值结果时临时创建的⼀个未命名的对象,C++中把这个未命名对象叫做临时对象。
#include <iostream>
using namespace std;
int main()
{
const int b = 10;
const int& a = b;
/*int& b = a;*/ // 权限放大,会报错
int c = 5;
const int& d = c * 3;
// int& d = c * 3; // 权限放大,会报错
return 0;
}
注意:
- 首先,引用的本质是给对象取别名,如果对象本身不可修改,它的引用可以修改导致他这个对象别修改了,就丢失了常性,相互矛盾
- 其次,不只是const修饰的变量才具有常性,只要是系统生成的临时对象都具有常性,如加减乘除运算、函数的的返回值。
5.指针和引用的关系
指针和引用各有特点互相不可替代
比如链表中只能使用指针,因为c++规定引用不能改变被引用的对象
• 语法概念上引⽤是⼀个变量的取别名不开空间,指针是存储⼀个变量地址,要开空间。
• 引⽤在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
• 引⽤在初始化时引⽤⼀个对象后,就不能再引⽤其他对象;⽽指针可以在不断地改变指向对象。
• 引⽤可以直接访问指向对象,指针需要解引⽤才是访问指向对象。
• sizeof中含义不同,引⽤结果为引⽤类型的⼤⼩,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8byte)
• 指针很容易出现空指针和野指针的问题,引⽤很少出现(也会出现,以后会介绍),引⽤使⽤起来相对更安全⼀些。
七、内联 inline
用inline修饰的函数就是内联函数,如果这个函数代码量较少,不存在递归,那么在编译时,编译器可能将在调用这个函数的位置直接展开成函数内部的代码。从而避免建立栈帧空间,提高效率。
注意:
- inline对编译器只是建议,最终决定在编译器,一般较长的函数或者递归函数编译器会直接忽略inline,不同编译器略有不同。
- vs编译器 debug版本下⾯默认是不展开inline的,这样⽅便调试,debug版本想展开需要设置⼀下。
- inline不建议声明和定义分离到两个⽂件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错。
用法:
inline int add(int& x, int& y )
{
return x + y;
}
八、nullptr
NULL实际是一个宏,在传统c头文件(stddef.h)中,可看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
这段代码的意义就是c中 NULL被定义为((void * ) 0),就是将0强制转换成void *,而在cpp文件中,NULL直接定义为0。
示例:
#include<iostream>
using namespace std;
void f(int x)
{
cout << "f(int x)" << endl;
}
void f(int* ptr)
{
cout << "f(int* ptr)" << endl;
}
int main()
{
f(0);
// 本想通过f(NULL)调⽤指针版本的f(int*)函数,但是由于NULL被定义成0,调⽤了f(int
// x),因此与程序的初衷相悖。
f(NULL);
f((int*)NULL);
// 编译报错:error C2665: “f”: 2 个重载中没有⼀个可以转换所有参数类型
// f((void*)NULL);
f(nullptr);
return 0;
C++中NULL可能被定义为字⾯常量0,或者C中被定义为⽆类型指针(void*)的常量。不论采取何种
定义,在使⽤空值的指针时,都不可避免的会遇到⼀些⿇烦,本想通过f(NULL)调⽤指针版本的
f(int*)函数,但是由于NULL被定义成0,调⽤了f(int x),因此与程序的初衷相悖。f((void*)NULL);
调⽤会报错。
为了解决这个问题,c++引入关键字nullptr,它是一个可以隐式转换成任意指针的空指针(注意不能转换成指针之外的其他类型)
ng namespace std;
void f(int x)
{
cout << “f(int x)” << endl;
}
void f(int* ptr)
{
cout << “f(int* ptr)” << endl;
}
int main()
{
f(0);
// 本想通过f(NULL)调⽤指针版本的f(int*)函数,但是由于NULL被定义成0,调⽤了f(int
// x),因此与程序的初衷相悖。
f(NULL);
f((int*)NULL);
// 编译报错:error C2665: “f”: 2 个重载中没有⼀个可以转换所有参数类型
// f((void*)NULL);
f(nullptr);
return 0;
C++中NULL可能被定义为字⾯常量0,或者C中被定义为⽆类型指针(void*)的常量。不论采取何种
定义,在使⽤空值的指针时,都不可避免的会遇到⼀些⿇烦,本想通过f(NULL)调⽤指针版本的
f(int*)函数,但是由于NULL被定义成0,调⽤了f(int x),因此与程序的初衷相悖。f((void*)NULL);
调⽤会报错。
为了解决这个问题,c++引入关键字**nullptr**,它是一个可以隐式转换成任意指针的空指针(注意**不能转换成**指针之外的**其他类型**)