在程序员中流行的一个说法,对C自增是C++,对C++自增是C#。
由此可见,C++是C的一种延伸,虽然说C++是一种面向对象编程语言,但这种面向对象是不完全的,起码是遗留了一些面向过程的痕迹,准确点说是带了一些C语言的痕迹。
这也是无法避免的。也是因为这种延伸和遗留造成了C++的难以理解。
如果给C++一个准确的说法,我更倾向于它是一种混合性语言,既有C语言的特点,也同时引入了面向对象的思想,视为C语言的改进和扩展。
让我们先来看一下,从C到C++有了哪些变化:
1、布尔类型(bool)
布尔类型是C++新增的一种基本数据类型。在标准的C语言中没有定义该类型,在C语言中如果需要使用bool类型,可以通过宏定义来自定义一个bool类型,定义语句如下:
#define bool int
#define false 0
#define true 1
在C++中对bool类型已经做出了定义。
bool类型是C++语言基本数据结构之一,两种主流编译器gcc和Visual C++给bool类型变量分配1个字节长度。
bool类型取值范围仅有两个值:true和false。在做逻辑运算时,默认非零即为ture。
Qt环境下的头文件是stdbool.h
/* Supporting <stdbool.h> in C++ is a GCC extension. */
#define _Bool bool
#define bool bool
#define false false
#define true true
定义bool类型变量也与其他基本数据类型变量的定义类似,如下所示:
#include <iostream>
using namespace std;
int main()
{
bool flag = true;
cout << flag << "," << sizeof(flag) << endl;
return 0;
}
执行结果:
1,1
2、命名空间(namespace)
对于命名空间的作用就是避免命名冲突。
好比1班有叫小明的同学,2班也有叫小明的同学,他们在各自班级范围内,都是可以区分的,一旦两个班级合并,两个小明但从名字上就无法区分了。
而实际程序开发中,每一个程序员都是独立书写代码的,不可避免会出现同名的变量或者函数(当然如果放在类中这个问题其实很好解决),这个时候就需要我们要说明是哪个范围中的什么变量或者函数,
拿小明来说,如果指明是1班的还是2班的,那么人就好区分了。
例如:
namespace Ming1{ //小李的变量声明
int flag = 1;
}
namespace Ming2{ //小韩的变量声明
bool flag = true;
}
这里在输入cin、输出的cout、换行endl体现很明显
#include <iostream>
//方式1
using namespace std;
int main()
{
cout << "Hello World!" << endl;
return 0;
}
//方式2
int main()
{
std::cout << "Hello World!" << std::endl;
return 0;
}
//方式3
using std::cout;
using std::endl;
int main()
{
cout << "Hello World!" << endl;
return 0;
}
3、输入cin、输出cout
C语言中的scanf和printf其实在C++依然可以沿用,但C++又设计了一套输入输出库,包含的头文件是iostream.h
在iostream中定义了用于输入输出的对象,例如常见的cin表示标准输入、cout表示标准输出、cerr表示标准错误。
cin、cout、cerr不是C++中的关键字,其本质是函数调用,它们的实现采用的是C++的运算符重载,其中cout和cerr的输出的目标是显示器,但不同的是cout是带有缓冲的,而cerr则不带缓冲。
#include <iostream>
using namespace std;
int main()
{
int num;
cout << "Hello World!" << endl;
cin >> num; //输入一个整型数据
cout << num << endl; // 输入num存放的数据
//连续输入
cin >> a >> b;
cout << a << "," << b << endl;
return 0;
}
4、引用(Reference)
引用是C++特有的,是对C语言的扩展。
引用可以看做是被引用对象的一个别名,在声明引用时,必须同时对其进行初始化。好像我们小时候,小伙帮之间互相取的绰号。
引用的声明方法如下:
类型标识符 &引用名 = 被引用对象
#include <iostream>
using namespace std;
int main()
{
int num = 1;
int &flag = num; //flag是num的引用
cout << num << " " << flag <<endl;
cout << &num << " " << &flag <<endl;
return 0;
}
可以看到flag和num的地址是一样的,类似C语言的指针。
引用的使用:
//通过引用修改变量num的值
flag = 20;
cout << num <<endl;
//如果不想通过引用的方式修改变量值,我们可以使用const
int a = 10;
const int &b = a;
b = 20; // error
形参的引用使用
这与我们C中地址的使用类似,也是看似传值实质传址。
#include<iostream>
using namespace std;
void swap(int &a, int &b);
int main()
{
int a = 10;
int b = 20;
cout<< a <<","<< b <<endl;
swap(a, b);
cout<< a <<","<< b <<endl;
return 0;
}
void swap(int &a, int &b)
{
int temp = a;
a = b;
b = temp;
}
image.png
返回值的引用
格式:
类型 &函数名(形参列表){ 函数体 }
注意:
1.引用作为函数的返回值时,必须在定义函数时在函数名前将&
2.用引用作函数的返回值的最大的好处是在内存中不产生返回值的副本
#include<iostream>
using namespace std;
float temp;
float fn1(float r){
temp=r*r*3.14;
return temp;
}
float &fn2(float r){ //&说明返回的是temp的引用,换句话说就是返回temp本身
temp=r*r*3.14;
return temp;
}
int main(){
float a=fn1(1.0); //返回值
//float &b=fn1(1.0); //用函数的返回值作为引用的初始化值
/* error: invalid initialization of non-const
* reference of type 'float&' from an rvalue of type 'float'*/
//(有些编译器可以成功编译该语句,但会给出一个warning)
float c=fn2(1.0); //返回引用
float &d=fn2(1.0); //用函数返回的引用作为新引用的初始化值
cout<<a<<endl; //3.14
//cout<<b<<endl; //3.14
cout<<c<<endl; //3.14
cout<<d<<endl; //78.5
return 0;
}
image.png
不能返回局部变量的引用。如上面的例子,如果temp是局部变量,那么它会在函数返回后被销毁,此时对temp的引用就会成为“无所指”的引用,程序会进入未知状态。
不能返回函数内部通过new分配的内存的引用。虽然不存在局部变量的被动销毁问题,但如果被返回的函数的引用只是作为一个临时变量出现,
而没有将其赋值给一个实际的变量,那么就可能造成这个引用所指向的空间(有new分配)无法释放的情况(由于没有具体的变量名,故无法用delete手动释放该内存),
从而造成内存泄漏。因此应当避免这种情况的发生
当返回类成员的引用时,最好是const引用。这样可以避免在无意的情况下破坏该类的成员。
可以用函数返回的引用作为赋值表达式中的左值
#include<iostream>
using namespace std;
int value[10];
int error=-1;
int &func(int n){
if(n>=0&&n<=9)
return value[n];//返回的引用所绑定的变量一定是全局变量,不能是函数中定义的局部变量
else
return error;
}
int main(){
func(0)=10;
func(4)=12;
cout<<value[0]<<endl;
cout<<value[4]<<endl;
return 0;
}
image.png
5、C++强制类型转换
C++有四个关键字static_cast、const_cast、reinterpret_cast和dynamic_cast。这四个关键字都是用于强制类型转换的。
1) static_cast用于数据类型的强制转换,强制将一种数据类型转换为另一种数据类型。例如将整型数据转换为浮点型数据。
int a = 10;
int b = 3;
double result = static_cast<double>(a) / static_cast<double>(b);
2) const_cast则正是用于强制去掉这种不能被修改的常数特性,但需要特别注意的是const_cast不是用于去除变量的常量性,而是去除指向常数对象的指针或引用的常量性,其去除常量性的对象必须为指针或引用。
#include<iostream>
using namespace std;
int main()
{
const int a = 10;
//int * p = &a; //: invalid conversion from 'const int*' to 'int*' [-fpermissive]
const int * p = &a;
int *q;
q = const_cast<int *>(p);
*q = 20;
cout <<a<<" "<<*p<<" "<<*q<<endl;
cout <<&a<<" "<<p<<" "<<q<<endl;
return 0;
}
image.png
将变量a声明为常量变量,同时声明了一个const指针指向该变量(此时如果声明一个普通指针指向该常量变量的话是不允许的,编译器会报错),
之后定义了一个普通的指针*q。将p指针通过const_cast去掉其常量性,并赋给q指针。之后我再修改q指针所指地址的值时,这是不会有问题的。
运行结果,指针p和指针q都是指向a变量的,指向地址相同,
而且经过调试发现0x28fe94地址内的值确实由10被修改成了20,why?
在程序某个地方出现了一个q这样的指针,它可以修改常量a,这是一件很可怕的事情的,
可以说是一个程序的漏洞,毕竟将变量a声明为常量就是不希望修改它。
“*q=20”语句为未定义行为语句,所谓的未定义行为是指在标准的C++规范中并没有明确规定这种语句的具体行为,
该语句的具体行为由编译器来自行决定如何处理。对于这种未定义行为的语句我们应该尽量予以避免。
#include<iostream>
using namespace std;
const int & search(const int * a, int n, int value);
int main()
{
int a[10] = {0,1,2,3,4,5,6,7,8,9};
int value = 5;
int &p = const_cast<int &>(search(a, 10, value));
if(p == NULL)
{
cout<< "没有该值在数组中" <<endl;
}else{
cout<< "发现该值在数组中 = "<< p <<endl;
}
return 0;
}
const int & search(const int * a, int n, int value)
{
for(int i=0; i<n; i++)
{
if(a[i] == value)
{
return a[i];
}
}
return NULL;
}
image.png
建议在C++中不要利用const_cast去掉指针或引用的常量性并且去修改原始变量的数值,这是一种非常不好的行为。
3) reinterpret_cast主要有三种强制转换用途:改变指针或引用的类型、将指针或引用转换为一个足够长度的整形、将整型转换为指针或引用类型。在使用reinterpret_cast强制转换过程仅仅只是比特位的拷贝。
int *a = new int;
double *d = reinterpret_cast<double *>(a);
4) dynamic_cast用于类的继承层次之间的强制类型转换。
6、内联函数(inline)
编译器会将内联函数调用处用函数体替换,类似C语言中的宏扩展。
内敛函数优缺点:
1)优点:有效避免函数调用的开销,程序执行效率更高。
2)缺点:如果被声明为内联函数的函数体非常大,则编译器编译后程序的可执行码将会变得很大。
我们通常会将一些频繁被调用的短小函数声明为内联函数。
注意:inline 关键字放在函数声明处不会起作用,inline 关键字应该与函数体放在一起:
void swap(int &a, int &b); //inline不要放在这里
inline void swap(int &a, int &b)
{
//函数体
}
7、动态内存申请和释放
C语言动态分配和释放内存的函数是malloc、calloc、free。
C++动态分配和释放内存是通过new、new[]、delete和delete[]操作符实现的,注意他们不是函数,是操作符
int *p = new int; //动态申请一个int类型空间,用p指针指向
delete p; //释放指针指向的int类型空间
int *A = new int[10]; //动态申请10个int类型的数组空间,用指针A指向
delete[] p; //释放10个int类型的数组空间
注意:new和delete,new[] 和 delete[] 成对出现。
8、异常处理
1) 抛出异常: 一个函数能够检测出异常并且将异常返回,这种机制称为抛出异常。
2) 异常捕获: 当抛出异常后,函数调用者捕获到该异常,并对该异常进行处理,称之为异常捕获。
throw关键字用于抛出异常,catch关键字用于捕获异常,try关键字尝试捕获异常。
抛出异常的基本语法:
throw 表达式;
捕获的基本语法:
try
{
//可能抛出异常的语句
}
catch (异常类型1)
{
//异常类型1的处理程序
}
catch (异常类型2)
{
//异常类型2的处理程序
}
// ……
catch (异常类型n)
{
//异常类型n的处理程序
}
实例
#include<iostream>
using namespace std;
enum index{underflow, overflow}; //上溢 下溢
int arrayIndex(int *arr, int n, int index);
int main()
{
int *arr = new int[10];
for(int i=0; i<10; i++)
{
arr[i] = i;
}
try
{
cout<<arrayIndex(arr, 10, 5)<<endl;
cout<<arrayIndex(arr, 10, -1)<<endl;
cout<<arrayIndex(arr, 10, 15)<<endl;
}
catch(index e)
{
if(e == underflow)
{
cout<<"上溢出"<<endl;
exit(-1);
}
if(e == overflow)
{
cout<<"下溢出"<<endl;
exit(-1);
}
}
return 0;
}
int arrayIndex(int *arr, int n, int index)
{
if(index < 0) throw underflow;
if(index > n-1) throw overflow;//第二个异常没有抛出,因为第一个异常抛出后被catch捕获后结束
return arr[index];
}
注意:当函数抛出一个返回值时,即使不用try和catch语句,异常还是会被处理的,系统会自动调用默认处理函数来执行。