第六章 C++对C的拓展2
6.1 const详解
6.1.1 const 修饰普通变量
被修饰的对象是只读的
-
const int a; //a的值是只读的 int const a;
-
const int * p; 该语句表示指向整形常量的 指针,它指向的值不能修改。
-
int const * p; 该语句与b的含义相同,表示指向整形常量 的指针,它指向的值不能修改。
-
int * const p; 该语句表示指向整形的常量指针,它不能再指向别的变量,但指向(变量)的值可以修改。
-
const int *const p; 该语句表示指向整形常量 的常量指针 。它既不能再指向别的常量,指向的 值也不能修改。
-
int const *const p; 表示指向整形常量 的常量指针 。它既不能再指向别的常量,指向的值也不能修改。
6.1.2 const修饰成员变量
const修饰类的成员变量,表示成员常量,不能被修改,同时它只能在初始化列表中赋值。
class A
{
const int nValue;
A(int x):nValue(x){};
};
6.1.3 const 修饰类的成员函数
使用const修饰类的成员函数,该函数为类的常成员函数, 它不改变对象的成员变量。
class B
{
public:
int x;
public:
void f() //void f() const
{
this->x++; // 允许在非常量成员函数中修改 x
}
};
注意:普通的全局函数不能用const修饰
6.1.4 const 修饰对象
1、const 修饰的对象为常量对象,其中的任何成员都不能被修改。
2、const 修饰的对象只能访问类中的const函数。
3、const 修饰的对象可以访问public成员变量,但是不能够修改。
#include <iostream>
using namespace std;
class B
{
public:
int x;
public:
void f() const
{cout << "f(): " << endl;}
void f1() {};
};
int main(int argc, char **argv) {
B b;
const B p = b;
p.f();
return 0;
}
6.1.5 const 修饰引用
void function(const TYPE& Var); //引用参数在函数内不可以改变
void function(const int& t)
{
t++; //[Error] increment of read-only reference 't'
}
//引用指向某个常量时,需要用const修饰
//invalid initialization of non-const reference of type 'int&' from an rvalue of type 'int'
int &a = 19;
const int &b = 19;
6.1.6 const 修饰函数返回值
const修饰函数返回值其实用的并不很多,它的含义和const修饰普通变量以及指针的含义基本相同。
-
函数返回值为普通变量和对象时使用const修饰没有意义,接收函数的返回值可以是const也可以不是,因为不论怎样都不可能通过函数返回值的接收者改变函数的返回值
-
函数返回值是被const修饰的指针,注意返回的地址不要为函数栈上的地址,接收函数的返回值必须用const修饰
-
函数返回值是被const修饰的引用时,接收函数的返回值可以是const也可以不是,如果接收函数的返回值是引用时必须用const修饰
#include <iostream>
#include <Cstring>
using namespace std;
//const 修饰成员变量
class A
{
public:
const int x; //成员常量 只能在初始化成员列表中被赋值
A():x(100)
{}
/* A a;
* a.x = 1000; //错!!
* */
};
//const 修饰成员函数
class B
{
public:
int x;
B():x(100){}
int getX() const
{
return x;
}
};
#if 0
//const 不能修饰全局函数
void func() const //error: non-member function 'void func()' cannot have cv-qualifier
{}
#endif
class C
{
public:
int x;
C():x(100)
{}
int getX() const
{return x;}
void setX(int x)
{this->x = x;}
};
//const修饰引用:目的是不希望通过引用改变被应用对象的内容
void func(const B &t)
{
// t.x++;
}
//const修饰函数的返回值
const int func2()
{
int x = 100;
return 100;
}
const B func3()
{
B b;
return b;
}
//file10000
const char *func4() //之所以用const修饰 是希望调用函数的用户不能够修改这个函数体内在堆上申请的空间中的内容
{
char *p = new char[10];
strcpy(p, "hello");
return p;
}
const int *func5()
{
int a[4];
return a; //返回栈上的地址是没有意义的!因为这块栈空间在函数调用结束后会被系统自动回收,所以我们使用一个指针变量来接收这个函数的返回值是没有意义的q
}
const B &func6(B &t) //返回值是引用时,等价于返回值是:const B *func6()
{
// B b;
//return b; //如果返回值是引用,return b 编译器在处理的时return &b,返回的是栈上的一个地址是有问题的!
return t;
}
int main() {
//const修饰函数的返回值
int x = func2(); //a = b (int = const int) 不能通过a将b的值修改,所以b是不是const的无所谓
cout << x << endl;
B b = func3();
cout << b.x << endl;
const char *p = func4(); // error: invalid conversion from 'const char*' to 'char*'
cout << p << endl;
// *p = 'H';
int a = 100;
const int *p2 = &a;
const int *p3 = p2;
// *p3 = 1000; //*p2 = 1000 a = 1000
B t = func6(b); //t = b 不可能通过t修改b
const B &t2 = func6(b); // B &t2 = b; 能够通过应用t2修改b
#if 0
//const修饰普通变量
const int a = 10; //变量a是只读的
//a = 100; //error: assignment of read-only variable 'a'
int x = 100;
const int *p = &x; //*p是只读的,指针p不能够改变他所指向的内存空间的内容
// *p = 10;
int const *p2 = &x; //等效于 const int *p2 = &x;
const int *const p3 = &x; //p3是只读的:p3这个指针一旦初始化后就不能够再指向别的内存空间了 *p3是只读的
//p3 = &a; //error: assignment of read-only variable 'p3'
//*p3 = 1000; //error: assignment of read-only location '*(const int*)p3'
int const *const p4 = &x; //等价于 const int *const p3 = &x;
const C c;
//c.x = 1000; // error: assignment of member 'C::x' in read-only object
//c.setX(); //error: passing 'const C' as 'this' argument discards qualifiers [-fpermissive]
cout << c.x << endl;
#endif
return 0;
}
6.2 extern "C"
6.2.1 extern "C"的含义
1、extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉 编译器,其声明的函数和变量可以在本模块或其它模块中使用。
2、通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。例如,如果模块B欲引用该模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样,模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在 链接阶段 中从模块A编译生成的目标代码中找到此函数。
3、extern "C" 包含双重含义,从字面上即可得到:首先,被它修饰的目标是“extern”的;其次,被它修饰的目标是“C”的。
6.2.2 extern "C"的作用
1、我们来回顾一下c语言编译器gcc和c++编译器g++在编译一个函数的时候的处理方法
我们新建三个文件,分别命名为:example.h example.c main.cpp
“example.c” 的内容如下:
#include "example.h"
int add( int x, int y )
{
return x + y;
}
"example.h" 的内容如下:
#ifndef C_EXAMPLE_H
#define C_EXAMPLE_H
extern int add(int x,int y);
#endif
“main.cpp" 的内容如下:
#include <iostream>
using namespace std;
#include "example.h"
int main(int argc, char* argv[])
{
add(2,3);
return 0;
}
接下来我们对这几个文件进行编译,分别执行如下命令:
gcc -c example.c
g++ -c main.cpp
g++ main.o example.o
我们会发现 编译报错:
2、在C++编写的源文件中使用extern "C"告诉编译器该文件中哪些函数是来外来的,并且告诉编译器这些函数需要使用”C语言“的规则进行编译。
将 main.cpp 修改成这样既可:
#include <iostream>
using namespace std;
extern "C"
{
#include "example.h"
}
int main(int argc, char* argv[])
{
add(2,3);
return 0;
}
或者将example.h修改成这样:
#ifndef C_EXAMPLE_H
#define C_EXAMPLE_H
#ifdef __cplusplus
extern "C" {
#endif
int add(int, int);
#ifdef __cplusplus
}
#endif
#endif
这样,编译器在编译”add(2,3);“这条代码的时候,会用C语言的规则进行编译,这样生成的符号也叫做"add",因为在example.o中存在"add"函数,所以编译能够通过。
6.3 nullptr
C++中为了 避免“野指针”(即指针在首次使用之前没有进行初始化)的出现,我们声明一个指针后最好马上对其进行初始化操作。如果暂时不明确该指针指向哪个变量,则需要赋予NULL值。除了NULL之外,C++11新标准中又引入了nullptr来声明一个“空指针”,这样,我们就有下面三种方法来获取一个“空指针”:
int *p1 = NULL;
int *p2 = 0;
int *p3 = nullptr;
为什么C++11要引入nullptr?它与NULL相比又有什么不同呢?
C/C++中的NULL到底是什么呢?
1、NULL在C++中的定义,NULL在C++中被明确定义为整数0:
/* Define NULL pointer value */
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else /* __cplusplus */
#define NULL ((void *)0)
#endif /* __cplusplus */
#endif /* NULL */
2、NULL在C中的定义在C中,NULL通常被定义为如下:
#define NULL ((void *)0)
也就是说NULL实质上是一个void *指针。
那么问题又来了,我们从一开始学习C++的时候就被告诫C++是兼容C的,为什么对于NULL C++却不完全兼容C呢?C++之所以做出这样的选择,根本原因和C++的函数重载机制有关。考虑下面这段代码:
void Func(char *);
void Func(int);
int main()
{
Func(NULL);
}
如果C++让NULL也支持void *的隐式类型转换,这样编译器就不知道应该调用哪一个函数。
为什么要引入nullptr
为了让c++传递空指针调用void Func(char *);传参时传 nullptr
void Func(char *);
void Func(int);
int main()
{
Func(nullptr);
}
由于我们经常使用 NULL表示空指针,所以从程序员的角度来看,Func(NULL)应该调用的是Func(char *)但实际上NULL的值是0,所以调用了Func(int)。nullptr关键字真是为了解决这个问题而引入的。
第七章 异常
7.1 异常的基本概念
异常是程序在执行期间产生的问题。C++ 异常是指在程序运行时发生的特殊情况,比如操作容器时下标越界等问题。
C++ 异常处理涉及到三个关键字:try、catch、throw。
-
throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
-
catch: 在您想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常。
-
try: 尝试执行语句块,它后面通常跟着一个或多个 catch 块。
如果有一个块抛出一个异常,捕获异常的方法会使用 try 和 catch 关键字。try 块中放置可能抛出异常的代码,try 块中的代码被称为保护代码。使用 try/catch 语句的语法如下所示:
#include <iostream>
using namespace std;
class MyException1 {}; // 自定义异常类1
class MyException2 {}; // 自定义异常类2
int main()
{
try
{
// 保护代码,可能会抛出异常
// 例如:throw MyException1(); // 抛出自定义异常1
// 或:throw MyException2(); // 抛出自定义异常2
}
catch (MyException1 e1)
{
// 捕获 MyException1 类型的异常,并进行处理
}
catch (MyException2 e2)
{
// 捕获 MyException2 类型的异常,并进行处理
}
catch (...)
{
// 捕获其他类型的异常(不是 MyException1 或 MyException2)
}
return 0;
}
如果 try 块在不同的情境下会抛出不同的异常,这个时候可以尝试罗列多个 catch 语句,用于捕获不同类型的异常
7.2 抛出异常
您可以使用 throw 语句在代码块中的任何地方抛出异常。throw 语句的操作数可以是任意的表达式,表达式的结果的类型决定了抛出的异常的类型。
以下是尝试除以零时抛出异常的实例:
double division(int a, int b)
{
if( b == 0 )
{
throw "Division by zero condition!";
}
return (a/b);
}
7.3 捕获异常
catch 块跟在 try 块后面,用于捕获异常。您可以指定想要捕捉的异常类型,这是由 catch 关键字后的括号内的异常声明决定的。
try
{
// 保护代码
}catch( ExceptionName e )
{
// 处理 ExceptionName 异常的代码
}
上面的代码会捕获一个类型为 ExceptionName 的异常。如果您想让 catch 块能够处理 try 块抛出的任何类型的异常,则必须在异常声明的括号内使用省略号 ...,如下所示
try
{
// 保护代码
}catch(...)
{
// 能处理任何异常的代码
}
哦?还有这么高级的!意思是如果在try代码块中产生了除0的异常,catch语句块也能捕获到洛?
Let me try try ...
try
{
// 保护代码
int x = 10/0;
}catch(...)
{
// 能处理任何异常的代码
cout << "10/0" << endl;
}
运行结果我们发现,并不是我们所想象的,程序还是因为10/0操作而异常终止了,所以这种方式并不能处理系统产生的异常,而只能获取我们使用throw抛出的异常,例如:
try
{
// 保护代码
// int x = 10/0;
throw "Division by zero condition!";
cout << "hello" << endl;
}catch(...)
{
// 能处理任何异常的代码
cout << "10/0" << endl;
}
在程序开发过程中我们常常会抛出很多异常,我们该如何精准得捕获到底是哪个异常发生了呢?由于我们抛出的异常为字符串,所以,当捕获该异常时,我们必须在 catch 块中使用 const char*
#include <iostream>
using namespace std;
double division(int a, int b)
{
if(b == 0)
{throw "Division by zero condition!";}
return (a / b);
}
double division1(int a, int b)
{
if(b == 0)
{throw "hahaha++++++++++++++++++ ";}
return (a/b);
}
int main()
{
int x = 50;
int y = 0;
double z = 0;
try{
z = division(x, y);
z = division1(x, y);
cout << "z:" << endl;
} catch (const char *msg) {
cerr << msg << endl;
}
return 0;
}
7.4 C++ 标准的异常
如果程序运行时抛出的是系统异常,我们又该如果捕获呢?
C++ 提供了一系列标准的异常,定义在 <exception>
中,我们可以在程序中使用这些标准的异常。它们是以父子类层次结构组织起来的。
下表是对上面层次结构中出现的每个异常的说明:
7.5 定义新的异常
您可以通过继承和重载 exception 类来定义新的异常。下面的实例演示了如何使用 std::exception 类来实现自己的异常。
#include <iostream>
#include <exception>
using namespace std;
class MyException : public exception
{
public:
const char * what () const throw ()
{
return "C++ Exception";
}
};
int main()
{
try
{
throw MyException();
}
catch(MyException& e)
{
cout << "MyException caught" << endl;
cout << e.what() << endl;
}
catch(exception& e)
{
//其他的错误
}
}