C是面向过程的编程语言
当写大型程序是,过程就会过于复杂
c++的曾用名“c with class”。它是面向对象的语言。
在c的基础上修改而成
java的原名叫做“c++ - -” 是在使用c++的过程中遇见的一些局限性,对其进行修改而成。
c#是为了和Java竞争产生的,公司不一样
c++支持c语言的全部头文件,同时,自己的头文件是在c的头文件基础上去掉“.h”在前面加上c
c语言 | c++ |
---|---|
stdio.h | iostream |
math.h | cmath |
string.h | cstring |
stdlib.h | cstdlib |
…… | …… |
命名空间时C++引入的可以由用户命名的作用域,用来处理程序中常见的同名冲突。
全局变量中不能有同名函数、变量和类
为了使同名类存在,就要加作用域,于是就有了命名空间
本质上,命名空间就是定义了一个范围。
namespace {代码}
通过空间名::访问
::
变量、函数……
先申明使用的命名空间(当多个命名空间里面的变量同名时,会发生二义性的问题)(一般不用)
using namespace <名字>
申明使用某个命名空间下面的某个变量或函数(变量或函数)
using
命名空间可以嵌套使用。
#include
//定义aqua空间
namespace aqua
{
int a = 6;
char name_1[5]="aqua";
}
//定义hydro空间
namespace hydro
{
int a = 6;
char name_2[6] = "hydro";
}
int main()
{
//1、通过空间名::访问
printf("%d\n", aqua::a); //6
//2、先申明使用某个命名空间里面的代码
using namespace aqua;
using namespace hydro;
printf("%s %s\n", name_1, name_2); //aqua hydro
//printf("%d\n",a); //指定不明确,发生二义性问题
//3、先申明使用某个命名空间下的某个变量或函数
using aqua::a;
printf("%d\n", a); //6
return 0;
}
std是什么?
std是个名称空间标示符,C++标准库中的函数或者对象都是在命名空间std中定义的,所以我们要使用标准函数库中的函数或对象都要使用std来限定。
对象cout是标准函数库所提供的对象,而标准库在名字空间中被指定为std,所以在使用cout的时候要加上std: : 。这样编译器就会明白我们调用的cout是名字空间std中的cout。
为什么将cout放到命名空间中?
是因为像cout这样的对象在实际操作中或许会有好几个,比如说你自己也可能会不小心定义了一个对象叫cout,那么这两个cout对象就会产生冲突。
C++使用一个预定义的全局对象来输入输出
在命名空间std
下,使用cout
cin
来输入输出(注意:它不是函数,是对象)
用<<
>>
表示数据的流向。数据从开口端流向尖头端
∴ 输出:cout 这个对象就代表黑窗口,数据流向黑窗口即输出。并且它可以自动识别数据类型。(<<
)
输入:cin也是如此,代表数据从屏幕流向变量中。(>>
)
#include //input output stream
using namespace std;//避免之后每次使用都加上 std::(方便一点)
int main()
{
//标准输出
cout << "name:\t" << "aqua" << endl; //name: aqua
cout<<"age:\t" << 18 << endl; //age: 18
//标准输入:
int age;
char name[20];
cin >> name >> age; //输入数据:aqua 18
cout << name <<" " << age;//aqua 18
return 0;
}
C++和C语言的基本数据类型几乎一样
char short int long long float double unsigned signed ...
yiC++中原生支持,不需要包含其他头文件,C++直接支持bool类型
布尔类型对象可以被赋予文字值true或false,所对应的关系就是真与假的概念,即1,0。
可以使用boolalpha
打印出bool类型的true或false
#include
using namespace std;
int main()
{
bool ok = true;
cout << ok << endl;// 1
cout << boolalpha << ok; //true
return 0;
}
void* p = NULL;
int* p1 = p;
int* pn = NULL;
void* pp = pn;
//无报错,无警告,完美
在C语言中,void*可以和其他类型指针相互转换!
void* p = NULL;
int* p1 = p; //错误 “初始化”: 无法从“void *”转换为“int *”
int* pn = NULL;
void* pp = pn; //正确 任意类型的指针都可以自动转为万能指针
在C++中,void*不能直接转换为其他类型的指针,但是可以把其他类型的指针转为void*
NULL属于 C 语言中的宏,后来 C++11 引入了 nullptr 关键字,都用来表示空指针。
在 C 语言中,NULL是一个宏,被定义为空指针;在C++中,被定义为0,定义形式如下所示:
在C语言中NULL会被定义成(void*)NULL,但是C++不允许直接将 void * 隐式转换到其他类型,NULL 只好被定义为 0。
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
我们来看一个C++中使用NULL的例子,代码如下所示:
#include
using namespace std;
void func(int x)
{
cout << __FUNCSIG__<< endl;
}
void func(char* px)
{
cout << __FUNCSIG__ << endl;
}
int main()
{
//都调用的整数版本的func函数
func(2); //void __cdecl func(int)
func(NULL); //void __cdecl func(int)
return 0;
}
从运行结果来看,无论是数字还是NULL都是调用的,参数为int类型的函数,这是毋庸置疑的,C++中NULL就是0。
但是这个结果更本不符合语义,我们传NULL,肯定是想传一个空指针进去的,而不是作为一个整数0,为此C++11引入了新的空指针关键字。
下面我们来修改一下上面的程序,将 NULL 替换为 nullptr,修改后如下所示:
int main()
{
func(2); //void __cdecl func(int)
func(nullptr); //void __cdecl func(char *)
return 0;
}
修改之后,运行结果正常!
看到这里你应该明白为什么 C++11 引入 nullptr 了吧!就是因为 NULL 在 C++ 程序中容易引起歧义!
C语言中,并没有真正的const,它本质上还是一个只读变量,并不是常量
佐证:
通过指针间接修改只读变量的值:
int* pt = (int*)#
*pt = 19;
printf("%d %d\n", num,*pt); //output:19 19
const int num = 18;
//num = 19 //error:不能修改const 对象
//int arr[num] //error:数组大小必须是常量
C++中的const并不能通过指针修改。
int* pt = (int*)#
*pt = 19;
cout << num << " " << *pt << endl; //output:18 19
明明已经通过指针修改了a值,为什么输出却没有变呢?
解释:
C++编译器当碰见常量声明时,在符号表中放入常量,那么如何解释取地址呢?(编译期间即可确定)
编译过程中若发现对const使用了&操作符,则给对应的常量分配存储空间(为了兼容C)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aRu2e827-1677665208030)(image-20210130153947983.png)]
常量存储在全局区,定义一个指针指向它,当修改指针指向的值时,这个指针会悄悄的给你在栈区开辟一块空间,并在那一块空间存储上你修改的值。但你并不能查看那块新空间的地址(指针任会显示全局区常量的地址)。
const 的奇葩情况
当给C++中的常量赋值一个变量时,它又变得和C语言一样了;(在程序运行期间分配内存)
int num = 20;
const int a = num; //赋值变量
int* p = (int*)&a;
*p = 21;
cout << a << " " << *p << endl; //output:21 21
C语言中,将一个常量(全局区)赋值给字符指针,会自动默认为这是一个常量字符指针。但C++中不能这样操作
char* name = "aqua"; //错误(在C语言中这是可以通过的写法)
const char*name ="aqua"; //正确
#include
using namespace std;
void show(const char* name) // 需改成:void show(const char* name)
{
cout << name << endl;
}
void test()
{
show("aqua"); //"const char *" 类型的实参与 "char *" 类型的形参不兼容
//void show(const char* name); //请把函数原型里的参数加上const
}
int main()
{
test();
return 0;
}
C++中初始化和赋值并不是同一个概念
初始化:创建变量时候赋予其一个初始值。
赋值:把对象(已经创建)的当前值擦除,而用一个新值来代替。
使用{}初始化。
使用{}初始化变量的好处是:基本上所有对象都可以用{}初始化,提供了统一的初始化方式
#include
using namespace std;
int main()
{
int a = 5;//(常规操作)
int a1{ 3 };//C++
int b{ 3.14 };//不同在于:使用这种写法,精度收缩时会直接报错
int num[3]{ 1,2,3 };
return 0;
}
使用()进行初始化。(不建议用这种方式初始化)
局限性:初始化数组是不能使用()来初始化。
#include
using namespace std;
int main()
{
int a1(3);
//int num[3]( 1,2,3 );
return 0;
}
C语言中是利用库函数malloc和free来分配和释放函数
C++提供了运算符new和delete来代替malloc和free。
malloc的职责仅仅是分配内存,new除了分配内存外,还干一件事,调用构造函数。
free的职责仅仅是释放内存,delete除了释放内存之外,还干一件事,调用析构函数。
#include
using namespace std;
int main()
{
//No.1申请普通的变量
int* p = (int*)malloc(sizeof(int));//C的方式
//int* p1 = new int; //不会自动初始化内存
int* p1 = new int{ 66 }//new:加上{}初始化内存;
cout << *p << " " << *p1 << endl;
//释放内存
free(p);
delete p1;
//No.2申请数组
//int* parr = new int[5];
int* parr = new int[5]{0};//并全部初始化为0;
//释放数组
//delete[] parr; //释放不完全
delete[] parr; //只有这样才能全部的释放数组
return 0;
}
一般来说,使用new申请空间时,是从系统的“堆”(heap)中分配空间。申请所得的空间的位置时根据当时的内存的实际使用情况决定的。但是,在某些特殊情况下,可能需要在程序员指定的特定内存创建对象,这就是所谓的“定位放置new”(placement new)操作。
定位放置new操作的语法形式不同于普通的new操作。例如,一般都用如下语句A* p=new A;申请空间,而定位放置new操作则使用如下语句A* p=new (ptr) A;申请空间,其中ptr就是程序员指定的内存首地址。
#include
using namespace std;
int main()
{
//定位放置。
char memorys[1024];
/*模拟定位放置
int* paqua = (int*)memorys; //后面写的所有,都存储在memorys这段内存中
*paqua = 90;
cout << *paqua<< endl;
char* pname = memorys + 4;
strcpy(pname, "aqua");
cout << pname<
C++中,三目运算符返回的是变量本身,而不是值。
C语言中。三目运算符返回的是一个值。(常量)
#include
using namespace std;
int main()
{
int a = 2;
int b = 3;
cout << (a > b ? a : b) << endl; //3
(a > b ? a : b) = 99;//这种操作在C语言中是不被允许的。报错:左操作数必须是左值。
cout << (a > b ? a : b) << endl; //99 //直接对三目运算符返回的变量b进行了赋值
cout << "a:" << a << endl; //a:2
cout << "b:" << b << endl; //b:99
return 0;
}
引用变量是一个别名,也就是说,它是某个已存在变量的另一个名字。一旦把引用初始化为某个变量,就可以使用该引用名称来操作变量。对引用的操作与对其所绑定的变量或对象的操作完全等价。
Type &refName = variable_name;
只能引用左值,可以修改
#include
using namespace std;
int main()
{
//引用是一种特殊的数据类型(和指针类似),用来给对想取别名
//如何定义引用
int age = 18;
int& rage = age;//这是定义了一个引用,引用了age对象
cout << age << " " << rage << endl; //18 18
cout << &age << " " << &rage << endl; //地址是相同的:表明本质上就是同一个东西
age = 88;
cout << age << " " << rage << endl; //88 88
cout << &age << " " << &rage << endl; //4个地址都是一样的
//只能引用左值(变量)
int& ra = 88;//错误的写法
return 0;
}
可左可右,不能修改
#include
using namespace std;
//2、定义函数参数
void foo(const int& a)
{
cout << a << endl;
}
int main()
{
//常引用,不可通过引用修改其值。用在函数形参中,对于函数里面不需要修改的实参,都可以声明为常引用
//定义常引用
const int& ra = 88;
//1、不可修改
ra=66;//错误
//2、定义函数参数
foo(ra);
foo(88);
return 0;
}
可左可右,可以修改。但是伴随着移动语义:资源权限的转移的问题。
#include
using namespace std;
void foo(const int& a)
{
cout << a << endl;
}
int main()
{
//右值引用(&&):可左可右,还能修改
int&& ra = 6;
cout << ra << endl; //6
ra = 66;
cout << ra << endl; //66
return 0;
}
int& refa; //错误 没有初始化
int a = 8;
int& refa = a; //正确
int a = 8,b = 9;
int& refa = a;
refa = b; //只是把b的值赋值给了refa,而不是让refa引用b
int& refc = 12; //错误 “初始化”: 无法从“int”转换为“int &”,非常量引用的初始值必须为左值
const int&refc =12; //正确
当然,也可以使用右值引用来引用常量;或者使用std::move()把左值转成右值
引用右值
int&& refr = 21;
引用经过std::move()转换过的变量
int a = 123;
int&& refr = 21;
常引用和右值引用有什么区别呢?
1,常引用引用的值是不可以修改的;但是右值引用引用的值是可以修改的!(大多数情况用常引用:函数参数)
2,右值引用一般用来实现移动语义(资源权限的转移)
通过使用引用来替代指针,会使 C++ 程序更容易阅读和维护
//在函数内部改变实参的值需要传变量的地址
void fun(int* n)
{
*n=18
}
//指针是非常危险的,因为指针所指向的内存空间,不确定,需要额外判断
fun(nullptr); //传nullptr 会发生中断,当然,你可以在函数里面判断是否是空,但是如果是野指针呢?
//在C++中,除了使用指针外,还可以通过引用来达到这个目的
void fun(int& n)
{
n=18
}
//可以用指针的引用替代二级指针
int& getAge()
{
int age = 18;
return age; //注意:不要返回局部变量的引用或地址,可以使用静态变量或全局变量替代
}
int& refAge = getAge();
refAge = 23;
引用如此神奇,那么引用的本质到底是什么呢?
C语言和C++语言都提供了枚举类型,两者是有一定区别。
有如下定义:
enum SHAPE {CIRCLE,RECT,LINE,POINT};
enum WEEK {MON,TUE,WED,THI,FIR,SAT,SUN};
允许非枚举值赋值给枚举类型,允许其他枚举类型的值赋值给另一个枚举类型
enum WEEK today = 3; //正确
today = CIRCLE; //正确
枚举具有外层作用域,容易造成名字冲突(在不同作用域不会冲突,但是遵循就近原则,访问不到外层作用域的枚举)
enum OTHER { RECT };//error C2365: “RECT”: 重定义;以前的定义是“枚举数”
int RECT = 12; //同上
if (CIRCLE == MON)
{
printf("oh.yes");
}
enum WEEK today = 3; //错误 error C2440: “初始化”: 无法从“int”转换为“main::WEEK”
today = CIRCLE; //错误 error C2440: “=”: 无法从“main::SHAPE”转换为“main::WEEK”
enum OTHER { RECT }; //错误 error C2365: “RECT”: 重定义;以前的定义是“枚举数”
int RECT = 12; //错误同上 但是可以通过枚举名访问指定的枚举属性
OTHER::RECT; //正确
if (CIRCLE == MON)
{
cout<<"oh.yes";
}
enum class SHAPE {CIRCLE,RECT,LINE,POINT};
enum class WEEK {MON,TUE,WED,THI,FIR,SAT,SUN};
cout<<SHAPCE::RECT<<endl; //输出 1
if (SHAPE::CIRCLE == WEEK::MON) //error C2676: 二进制“==”:“main::SHAPE”不定义该运算符或到预定义运算符可接收的类型的转换
{
cout<<"oh.yes";
}
C 枚举类型支持不同类型枚举值之间赋值、以及数字赋值、比较,并且具有外层作用域。
C++ 中枚举不允许不同类型的值给枚举类型变量赋值,但仍然支持不同类型之间枚举进行比较,枚举符号常量具有外层作用域。
C++ 强枚举类型不允许不同类型之间的赋值、比较,枚举常量值并不具有外层作用域。
#include
using namespace std;
enum Week
{
mon=1, tus, wes,fur,fri,sta,sun//乱写的英文,不具有任何具体含义
};
//新增的枚举类
enum class type
{
woman,
man,
child
};
int main()
{
//定义枚举变量
Week today = fur;//和C不同的是可以直接定义,不用取别名(typedef)。
//Week tom = 5;//错误:初始化无法从int 转为Week;但是在C语言中可以。
Week tom = Week(5);
//使用枚举值(enum)
mon;//1、直接使用枚举值
Week::mon;//2、通过枚举类型::使用枚举值
//使用枚举值(enum class)
type::child; //只能使用这种方式
child; //错误
return 0;
}
在 C++11 之前的版本中,定义变量或者声明变量之前都必须指明它的类型,比如 int、char 等;但是在一些比较灵活的语言中,比如 JavaScript、PHP、Python 等,程序员在定义变量时可以不指明具体的类型,而是让编译器(或者解释器)自己去推导,这就让代码的编写更加方便。
C++11 为了顺应这种趋势也开始支持自动类型推导了!C++11 使用 auto 关键字来支持自动类型推导。
注意:auto 仅仅是一个占位符,在编译器期间它会被真正的类型所替代。或者说,C++ 中的变量必须是有明确类型的,只是这个类型是由编译器自己推导出来的。
#include
using namespace std;
auto foo() //auto定义的函数必须写在前面
{
return 4;
}
int main()
{
int a = 10;
double d = 5.12;
//自动类型推导:帮助你判断该定义成什么类型。
//使用auto定义对象,必须赋值
auto a1 = 10;
auto d1 = 3.14;
auto name = "aqua";
foo();
return 0;
}
#include
using namespace std;
int main()
{
int nums[5] = { 1,2,3,4,5 };
//1、常规的for循环
//2、基于范围的for循环
for (int n : nums)//(int n (改成auto n也行更方便))是数组中元素的类型;相当于把数组中的值逐一赋值给n
{
cout << n << " ";
}
cout << endl; //1 2 3 4 5
for (int&n : nums)
{
cout << n+1 << " ";
}
cout << endl; //2 3 4 5 6
return 0;
}
特点:
typeid 运算符用来获取一个表达式的类型信息。
typeid 的操作对象既可以是表达式,也可以是数据类型,下面是它的两种使用方法:
typeid( dataType )
typeid( expression )
dataType 是数据类型,expression 是表达式,这和 sizeof 运算符非常类似,只不过 sizeof 有时候可以省略括号( )
,而 typeid 必须带上括号。
typeid 会把获取到的类型信息保存到一个 type_info 类型的对象里面,并返回该对象的常引用;当需要具体的类型信息时,可以通过成员函数来提取。
//获取一个普通变量的类型信息
int n = 100;
const type_info& nInfo = typeid(n);
cout << nInfo.name() << " | " << nInfo.raw_name() << " | " << nInfo.hash_code() << endl;
//获取一个字面量的类型信息
const type_info& dInfo = typeid(25.65);
cout << dInfo.name() << " | " << dInfo.raw_name() << " | " << dInfo.hash_code() << endl;
//获取一个普通类型的类型信息
const type_info& charInfo = typeid(char);
cout << charInfo.name() << " | " << charInfo.raw_name() << " | " << charInfo.hash_code() << endl;
//获取一个表达式的类型信息
const type_info& expInfo = typeid(20 * 45 / 4.5);
cout << expInfo.name() << " | " << expInfo.raw_name() << " | " << expInfo.hash_code() << endl;
本例中还用到了 type_info 类的几个成员函数,下面是对它们的介绍:
除此之外,还可以用 == 比较两个类型是否相等
如有以下定义:
char *str;
int a = 2;
int b = 10;
float f;
类型判断结果为:
类型比较 | 结果 | 类型比较 | 结果 |
---|---|---|---|
typeid(int) == typeid(int) | true | typeid(int) == typeid(char) | false |
typeid(char*) == typeid(char) | false | typeid(str) == typeid(char*) | true |
typeid(a) == typeid(int) | true | typeid(b) == typeid(int) | true |
typeid(a) == typeid(a) | true | typeid(a) == typeid(b) | true |
typeid(a) == typeid(f) | false | typeid(a/b) == typeid(int) | true |
函数调用时,需要跳转到函数的地址去执行,执行完成后返回到被调用函数,比较费时,因此,C++中提供了一种操作方式,允许编译时直接把函数替换到调用处,即内联函数。在函数前面加上inline申明为内联函数。
为什么使用内联函数?
内联函数没有普通函数调用时的额外开销(压栈,跳转,返回)
注意:
include
using namespace std;
#define m_min(a,b) a>b?a:b//(宏替换)
//内联函数(其实不用自己特意写,C++会帮你的)
inline int f_min(int a, int b)
{
return a > b ? a : b;
}
int main()
{
m_min(2, 3); //宏替换:这也算是内敛的过程,直接替换了后面的判断语句
f_min(2, 4);
return 0;
}
定义函数时可以给形参指定一个默认的值,这样调用函数时如果没有给这个形参赋值(没有对应的实参),那么就使用这个默认的值。也就是说,调用函数时可以省略有默认值的参数。如果用户指定了参数的值,那么就使用用户指定的值,否则使用参数的默认值。
#include
using namespace std;
//可以在定义函数的时候,给定形参一个默认值
//调用者如果没有传递参数,就会使用默认参数
void show(int num=0)
{
cout << num<< endl;
}
//如果有多个参数时,默认值必须从右往左依次指定(一旦某个参数有默认参数,那么它后面的所有参数都必须有默认参数)
int add(int a, int b, int c=7)
{
return a + b + c;
}
int main()
{
show(22);
show();
add(1, 2, 3);
add(1,2);
}
定义函数时,还可以给函数提供占位参数
void func(int a,int = 0)
{
cout<<a<<endl;
}
func(2);
本人认为:占位参数没什么用,唯一的用处就是占一个坑,为以后的函数扩充留下线索。
函数重载是指在同一作用域内,可以有一组具有相同函数名,不同参数列表的函数,这组函数被称为重载函数。
重载函数通常用来命名一组功能相似的函数。调用函数时,编译器会根据你后面参数的类型,自动为你识别调用哪个函数。
#include
using namespace std;
//定义一系列的函数来实现交换
namespace 常规
{
void int_swap(int& a, int& b)
{
int t = a;
a = b;
b = t;
}
void double_swap(double& a, double& b)
{
double t = a;
a = b;
b = t;
}
void string_swap(char* a, char* b)
{
char* t = new char[strlen(a)];
strcpy(t, a);
strcpy(a, b);
strcpy(b, t);
delete[] t;
}
}
//直接构成重载
void _swap(int& a, int& b)
{
int t = a;
a = b;
b = t;
}
void _swap(double& a, double& b)
{
double t = a;
a = b;
b = t;
}
void swap(char* a, char* b)
{
char* t = new char[strlen(a)];
strcpy(t, a);
strcpy(a, b);
strcpy(b, t);
delete[] t;
}
int main()
{
int a = 3, b = 4;
_swap(a, b);
cout<< a << " " << b << endl;
}
为了估计哪个重载函数最适合,需要依次按照下列规则来判断:
遇见实参与形参不匹配的情况时,会自动进行转换
在给重载函数指定默认参数时,要考虑是否会和别的重载函数冲突
void fun(int a)
{
cout << "fun(int a) " << a << endl;
}
void fun(int a, int b = 8)
{
cout << "fun(int,int =8) " << a <<" "<< b << endl;
}
int main()
{
//fun(5); //error C2668: “fun”: 对重载函数的调用不明确
fun(5, 6);//正确
return 0;
}