#include<iostream>
using namespace std;
int main() {
cout << "helloworld" << endl;
return 0;
}
一、C++中的头文件
(一)climits
头文件climits
(在老式实现中为limits.h
)定义了表示各种变量限制的符号名称。例如,INT__MAX
为int
的最大取值,CHAR_BIT
为字节的位数。
常用类型最大值的符号常量 | 表示 |
---|---|
CHAR_MAX | char 的最大值 |
SHRT_MAX | short 的最大值 |
INT_MAX | int 的最大值 |
LONG_MAX | long 的最大值 |
LLONG_MAX | long long 的最大值 |
常用类型最小值的符号常量 | 表示 |
---|---|
CHAR_MIN | char 的最小值 |
SHRT_MIN | short 的最小值 |
INT_MIN | int 的最小值 |
LONG_MIN | long 的最小值 |
LLONG_MIN | long long 的最小值 |
带符号与无符号的符号常量 | 表示 |
---|---|
SCHAR_MAX | singed char 的最大值 |
SCHAR_MIN | signed char 的最小值 |
UCHAR_MAX | unsigned char 的最大值 |
USHRT_MAX | unsigned short 的最大值 |
UINT_MAX | unsigned int 的最大值 |
ULONG_MAX | unsigned 的最大值 |
ULLONG_MAX | unsigned long 的最大值 |
二、引用
(一)左值引用与右值引用
——左值与右值
在 C++ 或者 C 语言中,一个表达式根据其使用场景不同,分为左值表达式和右值表达式,通常情况下,判断某个表达式是左值还是右值,常有以下 2 种方法:
- 可位于赋值号(=)左侧的表达式就是左值;反之,只能位于赋值号右侧的表达式就是右值。值得一提的是,C++ 中的左值也可以当做右值使用
- 有名称的、可以获取到存储地址的表达式即为左值;反之则是右值。
如下,a 和 b 是变量名,且通过 &a 和 &b 可以获得他们的存储地址,因此 a 和 b 都是左值;反之,字面量 5、10,它们既没有名称,也无法获取其存储地址(字面量通常存储在寄存器中,或者和代码存储在一起),因此 5、10 都是右值。
int a = 5; 5 = a; //错误,5 不能为左值 int b = 10; // b 是一个左值 a = b; // a、b 都是左值,只不过将 b 可以当做右值使用
正常情况下,C++的引用是给已有变量起别名,即左值引用
int num = 10;
int &b = num; //正确
int &c = 10; //错误
实际开发中我们可能需要对右值进行修改(如实现移动语义),显然左值引用的方式是行不通的。C++11 标准新引入了另一种引用方式,称为右值引用,用 "&&" 表示。
和声明左值引用一样,右值引用也必须立即进行初始化操作,且只能使用右值进行初始化
和常量左值引用不同的是,右值引用还可以对右值进行修改
三、函数
(〇)基本概念
#include<iostream>
#include<cmath>
using namespace std;
int main() {
double number = sqrt(6.25);
return 0;
}
表达式sqrt(6.25)
将调用sqrt()
函数。表达式sqrt(6.25)
被称为函数调用,被调用的函数叫做被调用函数(called function),包含函数调用的函数叫做调用函数(calling function)。main函数是调用函数,sqrt函数是被调用函数。
函数原型语句只描述函数接口,即发送给函数的信息和返回的信息。C+ +编译器需要根据函数原型知道函数的参数类型和返回值类型(函数是返回整数、字符、小数还是别的什么?)以便解释返回值。如下便是一种函数原型
void fun(int); //函数原型
函数原型有2种提供方法:
-
在源代码文件中输入函数原型。通常把原型放到
main()
定义之前,将代码放在main()
的后面。#include<iostream> void fun(int); //函数原型 int main(){ ...; fun(a); ...; return 0; } //函数定义 void fun(int n){ ...; }
-
包含(include)定义了原型的头文件;
函数定义中包含的是函数的代码,如计算平方根的代码。如下便是函数定义。
//函数定义
void fun(int n){
...;
}
C和C++将库函数的原型和定义分开了。库文件中包含了函数的编译代码,而头文件中则包含了原型。
(一)main函数
main()
的返回值是返回给操作系统。很多操作系统都可以使用程序的返回值。例如,UNIX外壳脚本和Windows命令行批处理文件都被设计成运行程序,并测试它们的返回值(通常叫做退出值)。
——书写格式
ANSI/ISO C++标准规定,如果编译器到达main()
函数末尾时没有遇到返回语句,则认为main()
函数以return 0;
结尾,且这条隐含的返回语句只适用于main()
函数,而不适用于其他函数。
return0; //不合法
return(0); //合法
return (0); //合法
intmain(); //不合法
int main() //合法
int main ( ) //合法
int main(void) //合法
(二)带默认参数值的函数
-
默认参数不能在声明(即函数原型)和定义中同时出现
/*错误*/ void fun1(int a=10); void fun1(int a=10){ ...... } /*正确*/ void fun2(int a=10); void fun2(int a){ ...... }
-
默认参数必须从函数参数的右边向左边使用
/*错误*/ void fun3(int a=5, int b, int c); void fun4(int a, int b=5, int c); /*正确*/ void fun1(int a, int b=10); void fun2(int a, int b=10, int c=20);
(三)内联函数
用关键字 inline
放在函数定义(注意是定义而非声明,下文继续讲到)的前面即可将函数指定为内联函数。
如果一个函数是内联的,那么在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方。相比普通的函数调用运行时需要开辟栈空间等等,内联函数可以减少系统开销,加速代码运行。
特点:
- 适用于代码量小的简单函数
- 内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。以下情况不宜使用内联:
- 如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
- 如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。
四、循环语句
(一)基于范围的for循环
#include<iostream>
using namespace std;
int main() {
char name[11] = "Em0s_Er1t";
for (const auto &e : name) {
cout << e; //输出数组的每个元素
}
return 0;
}
/*输出:
Em0s_Er1t
*/
五、运算符
(一)sizeof
sizeof 运算符返回类型或变量的长度(在内存中所占用的存储空间),单位为字节(byte)。
#include<iostream>
using namespace std;
int main() {
cout << "int is " << sizeof(int) << " bytes." << endl;
cout << "shrot is " << sizeof(short) << " bytes." << endl;
cout << "long is " << sizeof(long) << " bytes" << endl;
cout << "long is " << sizeof(long long) << " bytes" << endl;
return 0;
}
/*输出:
int is 4 bytes.
shrot is 2 bytes.
long is 4 bytes
long is 8 bytes
*/
(二)类型转换
C++通常在如下情况下自动执行类型转换
- 将A类算术类型的值赋给B类算术类型的变量时;
- 表达式中包含不同的类型
- 将参数传递给函数时
类型转换过程中会出现潜在的问题
1. 普通初始化与赋值操作的类型转化
将A类算术类型的值赋给B类算术类型的变量时,值将被转换成B类型。
将一个值赋给值取值范围更大的类型(如short转long)通常不会导致什么问题,但其它情况会导致一些问题(如下)。
转换 | 问题 |
---|---|
较大的浮点类型 \(\Rightarrow\) 较小的浮点类型(如double转float) | 精度降低,原来的值可能超出目标类型取值范围,最终结果不确定。 |
浮点类型 \(\Rightarrow\) 整型 | 小数部分丢失,原来的值可能超出目标类型的取值范围,最终结果不确定。 |
较大的整型 \(\Rightarrow\) 较小的整型(如long转short) | 原来的值可能超出目标类型取值范围,通常只复制右边的字节 |
2. 列表初始化下的类型转换
列表初始化引发的类型转换并不允许缩窄(如不允许将浮点型转换成整型)。
在不同的整型之间转换或将整型转换为浮点型可能被允许,此时顺利转换的条件是编译器知道目标变量一定能够正确地存储赋给它的值(如可将long型变量初始化为int值,因为long总是至少与int一样长,相反方向转换可能被允许)
/*非法,尽管10可以存入char型变量,但编译器看来,a是int型变量,无论存储的值是大是小都一视同仁,不被允许缩窄成char型*/
int a = 10;
char b = { a };
/*合法*/
int a = 10;
char b = a;
3. 表达式中的转换
一些类型只要一出现在表达式中便会被转换,如C++将bool、char、 unsigned char、signed char和short值转换为
int(如true 被转换为1,false 被转换为0),这些转换被称为整型提升(integral promotion)
将不同类型进行算术运算时,会将较小的类型将被转换为较大的类型,C++11规定按如下流程进行转换:
-
如果有一个操作数的类型是long double,则将另一个操作数转换为long double。
-
否则,如果有一个操作数的类型是double,则将另一个操作数转换为double.
-
否则,如果有一个操作数的类型是float,则将另一个操作数转换为float。
-
否则,说明操作数都是整型,因此执行整型提升。
-
在这种情况下,如果两个操作数都是有符号或无符号的,且其中一个操作数的级别比另一个低,
则转换为级别高的类型。有符号整型按级别从高到低依次为long long、long、int、short 和signed char。 无符号整型的排列顺序与有符号整型相同。类型char、signed char和unsigned char的级别相同,类型bool的级别最低。wchar_t、char16_t和char32_t的级别与其底层类型相同。
-
如果一个操作数为有符号的,另一个操作数为无符号的,且无符号操作数的级别比有符号操作数
高,则将有符号操作数转换为无符号操作数所属的类型。 -
否则,如果有符号类型可表示无符号类型的所有可能取值,则将无符号操作数转换为有符号操作
数所属的类型。 -
否则,将两个操作数都转换为有符号类型的无符号版本。
六、变量与数据类型
(一)单值变量的初始化
1. 列表初始化(list-initialization)
C++11使得大括号初始化器可以任何类型的初始化,可以使用等号=
,也可以不使用。
/*用大括号初始化器可以对单值变量初始化*/
int a{7}; //将a初始化为7
int b = {7}; //将b初始化为7
大括号内可以不包含任何东西,在这种情况下,变量被初始化为0。
int a = {}; //将a初始化为0
int b{}; //将b初始化为0
2. 圆括号初始化
/*也可以用小括号*/
int c(7); //将c初始化为7
int d = (7); //将d初始化为7
string s(5,'c'); // s被初始化为"ccccc"
(二)字符
1. wchar_t
传统的字符数据类型为char,占用一个字节,存放的数据内容为ASCII编码,最多可以存放255种字符,基本的英文以及常用字符都可以涵盖,随着计算机在国际范围内普及,大量使用其它语言的计算机用户也纷纷出现,传统的ASCII编码已经无法满足人们的使用,包含更多字符的字符集随之出现,因此一种新的字符存放类型wchar_t应运而生。
wchar_t为宽字符类型或双字符类型,它占用两个字节,因此能够存放更多的字符。
- 给wchar_t类型的变量初始化或者赋值时,常量需要加上前缀
L
,如果没有L
,程序将会将wchar_t
转换为char
- 对于ASCII码能够存放的数据类型,其高位存放的数据为0x00
- char类型的字符串以
\0
结尾,wchar_t类型的字符串以\0\0
结尾 - cin和cout将输入和输出看作是char流,因此不适于用来处理wchar_t类型,如今已经提供了与之作用相似的工具wcin 和wcout,可用于处理wchar_t流。
(三)布尔型
布尔变量的值为true
或false
- C++将非0值解释为
true
,将0解释为false
,任何非0值可以被隐式转换成true,0被隐式转换成false; - 字面值
true
和false
都可以通过提升转换为 int 类型,true 被转换为1,而false被转换为0;
int a=true; //a被初始化为1
int b=false; //b被初始化为0
bool c=100; //c被初始化为true
bool d=0; //d被初始化为false
(四)数组
1. 定义与初始化
数组定义的通用格式如下
类型说明符 数组名[数组大小] //数组大小可以以整型常数、const值、常量表达式的形式,但不能是变量
数组初始化采用的是列表初始化:
(1)一维数组初始化
-
只有定义数组时才能初始化,不能将一个数组赋给另一个数组。
int a[3] = { 1,2,3 }; //合法 int b[3]; //合法 b = a; //非法
-
初始化数组时提供的值可以少于数组的元素数目,若只对数组的一部分进行初始化,编译器将其它元素置为0。
#include<iostream> int main() { int a[3] = { 1 }; std::cout << a[2]; return 0; } /*输出: 0 */
-
在列出全部数组元素初值时,可以不指定数组长度,C++编译器将计算元素的个数。
#include<iostream> int main() { int a[] = { 1,2,3,4 }; //编译器将把数组a视作包含4个元素 int num = sizeof(a) / sizeof(int); //计算数组a所含元素的个数 std::cout << "the array 'a' has " << num << " elements." << std::endl; return 0; } /*输出; the array 'a' has 4 elements. */
-
用大括号初始化数组时,可以省略等号(=),且可以在大括号内不包含任何元素,此时所有元素被设置为0
#include<iostream> int main() { int a[2]{}; std::cout << a[0] << std::endl; return 0; } /*输出: 0 */
-
列表初始化引发的类型转换禁止缩窄变换。
long a[] = {25,92, 3.0}; //非法,浮点数转整数是缩窄操作 char b[4] { 'h','i', 1122011, '\0'}; //非法,1122011超出char变量的取值范围 char c[4] { 'h','i',112, '\0'}; //合法
(2)二维数组初始化
-
将所有初值写在一个{}内, 按顺序初始化
static int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12}; static int a[3][4]={1,2,,,,,,,,10,11}; //非法
-
分行列出二维数组元素的初值
static int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
-
可以只对部分元素初始化
static int a[3][4]={{1},{0,6},{0,0,11}};
-
列出全部初始值时,第1维下标个数可以省略,第2维下标不可以省略。
static int a[][4]={1,2,3,4,5,6,7,8,9,10,11,12}; static int a[][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12};
-
static数组默认初始化为0
-
初始化数组时提供的值可以少于数组的元素数目,若只对数组的一部分进行初始化,编译器将其它元素置为0。
2. 对象数组
定义对象数组
类名 数组名[元素个数];
访问对象数组元素
数组名[下标].成员名
初始化
- 当数组中每一个对象被创建时,系统都会调用类构造函数初始化该对象。
- 当数组中每一个对象被删除时,系统都要调用一次析构函数。
Point a[2]= {Point(1 ,2),Point(3,4)}; //调用构造函数。a[0]先于a[1]构造,a[1]先于a[0]析构
Point b[2] //调用默认构造函数
(五)指针
指针:内存地址,用于间接访问内存单元
指针变量:用于存放地址的变量
1. 定义与初始化
-
用变量地址作为初值时,该变量必须在指针初始化之前已声明过,且变量类型应与指针类型一致。
-
可以用一个已有合法值的指针去初始化另一个指针变量。
-
不要用一个内部非静态变量去初始化static 指针,尽管这样是合法的。
-
允许定义或声明指向void类型的指针,该指针可以被赋予任何类型的地址。
-
当定义了一个指针,但其指向尚未明确,我们需要将其初始化为空指针。
>野指针
“野指针”又称“悬挂指针”,指的是没有明确指向的指针。野指针往往指向的是那些不可用的内存区域,这就意味着像操作普通指针那样使用野指针(例如 &p),极可能导致程序发生异常。实际开发中,避免产生“野指针”最有效的方法,就是在定义指针的同时完成初始化操作
-
对于指向常量的指针,不能通过其改变所指变量的值,但指针本身可以改变,可以指向别的变量
static int i;
void *general; //见"4"
static int *ptr = &i; //见“1”(指针变量ptr中只能存储static int型变量的内存空间的地址)
*ptr = 3; //相当于 i=3;
/*见“5”,下面3种初始化为空指针的方法等价*/
int *p=nullptr;
int *p=0;
int *p=NULL;
/*见“6”*/
int a, b;
const int *p1 = &a; //p1是指向常量的指针
p1 = &b; //正确,p1本身的值可以改变
*p1 = 1; //编译时出错,不能通过p 1改变所指的对象
(六)字符串
字符串一定是以ASCII码为0的空字符\0
结尾的一系列连续字符,C++有很多处理字符串的函数,其中包括cout
所使用的那些函数,它们都逐个处理字符串中的字符,直到到达空字符\0
为止。如果使用cout显示如下name2
这样的字符串,则将显示前9个字符,发现空字符后停止。
但是,如果使用cout显示如下的name1
数组(它不是字符串,但会被视作字符串处理),cout 将打印出数组中的9个字符,并接着将内存中随后的各个字节解释为要打印的字符,直到遇到空字符为止。由于空字符(实际上是被设置为0的字节)在内存中很常见,因此这一过程将很快停止,但尽管如此,还是不应将不是字符串的字符数组当作字符串来处理。
将字符数组初始化为字符串可以用双引号将字符串括起来(如下的name3
),这就隐式地包含了空字符\0
。
/*区分字符串与字符数组*/
#include<iostream>
int main()
{
char name1[] = { 'E','m','0','s','_','E','r','1','t' }; //不是字符串而是字符数组
char name2[] = { 'E','m','0','s','_','E','r','1','t','\0' }; //是字符串
char name3[] = "Em0s_Er1t"; //是字符串
std::cout << name1 << std::endl;
std::cout << name2 << std::endl;
std::cout << name3 << std::endl;
return 0;
}
/*输出:
Em0s_Er1t烫烫烫?N9???Y
Em0s_Er1t
Em0s_Er1t
*/
1. strlen
计算可见字符(不包括空字符\0
)的长度
2. cin.getline
(七)自定义数据类型
C++语言常用的自定义数据类型有枚举、联合体、结构体等。
1. 枚举类型(enum)
枚举类型的数据类型定义如下
enum <枚举类型名> {
枚举量0, 枚举量1,..., 枚举量n-1
}
枚举类型变量的取值范围只能是这个列表中这几个枚举量,编译器会将枚举值按照他们定义时排列的先后顺序将他们分别与 \(0\sim n-1\) 的整数关联起来。
#include <iostream>
using namespace std;
enum weekdays { Monday, Tuesday, Wednesday, Thursday, Friday };
int main() {
weekdays today;
today = Monday;
cout << today << endl;
today = Tuesday;
cout << today << endl;
return 0;
}
/*输出:
0
1
*/
以下是利用枚举类型配合switch增强代码可读性的一个例子
#include <iostream>
using namespace std;
enum weekdays { Monday, Tuesday, Wednesday, Thursday, Friday };
int main() {
int today;
cin >> today;
switch (today) {
case Monday:
cout << "Today is Monday";
break;
case Tuesday:
cout << "Today is Tuesday";
break;
case Wednesday:
cout << "Today is Wednesday";
break;
case Thursday:
cout << "Today is Thursday";
break;
case Friday:
cout << "Today is Friday";
break;
default:
cout << "Invalid selection" << endl;
}
return 0;
}
枚举类
用enum
创建的数据类型,会产生如下命名问题
enum Color{black,white,red}; //black、white、red作用域和color作用域相同
int white; //错误,white关键字被占用
于是枚举类enum class
应运而生,用enum class
创建的数据类型的枚举量在外部被使用需要加上作用域限定
enum class Color{black,white,red}; //black、white、red作用域仅在大括号内生效
int white; //正确,这个white并不是Color中的white
Color c = white; //错误,在作用域范围内没有white这个枚举量
Color c = Color::white; //正确
auto c = Color::white; //正确
2. 联合类型(union)
在一个联合体内,我们可以定义多个不同类型的成员,这些成员将会共享同一块内存空间,该内存空间的大小是其最大成员的内存空间大小,因此后赋值的会将前面赋值的数据覆盖。
联合类型的数据类型定义如下
union <联合类型名> {
<成员表>
};
举个例子
#include <iostream>
using namespace std;
union author{
unsigned long birthday;
float score;
};
int main() {
author me;
me.birthday = 20010101;
cout << me.birthday << endl;
me.score = 90.5;
cout << me.birthday << endl;
cout << me.score << endl;
return 0;
}
/*输出:
20010101
1119158272
90.5
*/
3. 结构体
(八)auto类型
七、常量
(一)整型常数
代码中整数的书写有3种方式
- 若从左往右第一位为 \(1\sim 9\),则这个整数是十进制数;
- 若从左往右第一位为 \(0\),
- 第二位是
x
或者X
,则这个整数是十六进制; - 第二位是 \(1\sim 7\),则这个整数是八进制。
- 第二位是
#include<iostream>
using namespace std;
int main() {
int a = 23, b = 0x23, c = 023;
cout << a << endl;
cout << b << endl;
cout << c << endl;
return 0;
}
/*输出:
23
35
19
*/
后缀是放在数字常量后面的字母,用于表示类型。
-
整数后面的
l
或L
后缀表示该整数为long常量 -
u
或U
后缀表示unsigned int
常量 -
ul
(u和l顺序可互换,且大小写均可)表示unsigned long
常量(由于小写1看上去像1,因此应使用大写L
作后缀)。例如,在int 为16位、long 为32位的系统上,数字22022被存储为int,占16位,数字22022L被存储为long,占32位。同样,22022LU和22022UL都被存储为unsigned long。C++11 提供了用于表示类型long long的后缀II和LL,还提供了用于表示类型unsigned long long 的后缀
ull
、Ull
、uLL
和ULL
。
(二)转义字符
转义字符 | 意义 | ASCII码值(十进制) |
---|---|---|
\a |
响铃(BEL) | 007 |
\b |
退格(BS) ,将光标的当前位置移到前一列 | 008 |
\f |
换页(FF),将光标当前位置移到下页开头 | 012 |
\n |
换行(LF) ,将光标当前位置移到下一行开头 | 010 |
\r |
回车(CR) ,将光标当前位置移到本行开头 | 013 |
\t |
水平制表(HT) (跳到下一个TAB位置) | 009 |
\v |
垂直制表(VT) | 011 |
\\ |
代表一个反斜线字符''' | 092 |
\' |
代表一个单引号(撇号)字符 | 039 |
\" |
代表一个双引号字符 | 034 |
\? |
代表一个问号 | 063 |
\0 |
空字符(NULL) | 000 |
\ddd |
1到3位八进制数所代表的任意字符 | |
\xhh |
1到2位十六进制所代表的任意字符 |
若某字符既有数字转义序列也有符号转义序列(如
\0x8
和\b
),则应使用符号序列,因为数字表示与特定的编码方式(如ASCII码)相关,而符号表示适用于任何编码方式,其可读性也更强。、
#include<iostream>
using namespace std;
int main() {
int code;
cout << "\aPlease Enter The Code: ________\b\b\b\b\b\b\b\b";
cin >> code;
cout << "\aThe Code Is " << code << endl;
return 0;
}
(三)用const修饰的变量
定义变量时用const限定修饰后,该变量的值不能被改变,在整个作用域中都保持固定。
const int a;a = 10; //(错误)常量a不能被修改
const int a=10; //(正确)
(四)浮点数
浮点数常量可以采用下面2种方式表示:
- 标准小数点表示法
- E表示法(科学计数法):
d.dddE+n
指的是将小数点向右移n位,而d.dddE-n
指的是将小数点向左移n位。之所以称为“浮点”,就是因为小数点可移动。
默认情况下浮点常量属于double类型,如果希望常量为float 类型,需要使用f或F后缀。对于long double 类型,可使用l
或L
后缀。
八、
九、
十、类与对象
类描述了一种数据类型的全部属性(包括可使用它执行的操作),对象是根据这些描述创建的实体。在不考虑静态成员的情况下,当需要使用一个类的功能时,只能通过定义一个对象才能使用
类的描述指定了可对类对象执行的所有操作,但要对特定对象执行这些允许的操作,需要给该对象发送消息。C++提供了两种发送消息的方式:
-
使用类方法(本质上就是函数调用);
-
重新定义运算符;
cin 和cout采用的就是这种方式,
cout << "helloworld!";
使用重新定义的<<
运算符将要打印的消息"helloworld!"
发送给cout
。
(一)类的定义
class 类名称{
public:
公有成员(外部接口)
private:
私有成员(一些数据、辅助作用的函数)
protected:
保护型成员(继承关系相关)
}
-
公有类型成员:在关键字public后面声明,是类与外部的接口,任何外部函数都可以访问公有类型数据和函数。
-
私有类型成员:在关键字private后面声明,只允许本类中的函数访问,而类外部的任何函数都不能访问。
如果紧跟在类名称的后面声明私有成员,则关键字private可以省略。
-
保护型成员:
(二)类的基本规则
-
类内的成员函数可以直接访问类的数据成员(类的成员函数共享类的数据成员);
-
类的成员函数可以在类内说明函数原型(函数申明),在类外给出函数体实现(需要在函数名前加上作用域限定“
类名::
”);允许声明重载函数和带默认参数值的函数
-
类的成员函数也可以在类内部实现,但不推荐这种做法;
在类内部实现成员函数则自动被视作内联函数
-
外部需要使用
对象名.成员名
方式访问public属性的成员; -
类的成员函数内部可以访问任何同类对象的私有成员;
用友元可以打破这层约束,见“友元”
#include<iostream>
using namespace std;
class Clock {
int hour, minute, second;
public:
void setTime(int newH = 0, int newM = 0, int newS = 0); //见"2"
void showTime() { //见"3"
cout << hour << ":" << minute << ":" << second << endl; //见"1"
}
void compTime(const Clock &time);
};
void Clock::setTime(int newH, int newM, int newS) { //见"2"
hour = newH; //见"1"
minute = newM; //见"1"
second = newS; //见"1"
}
void Clock::compTime(const Clock &time) {
if (time.hour == hour && time.minute == minute && time.second == second) //见"5"
cout << "equal" << endl;
else
cout << "not equal" << endl;
}
int main() {
Clock myclock;
/*通过调用setTime函数间接完成对对象的数据成员的赋值*/
myclock.setTime(20, 15, 23); //见"4"
myclock.showTime(); //见"4"
return 0;
}
(三)类的初始化及相关问题
1. 构造函数(Constructor)
在对象被创建时自动调用构造函数使用特定的值构造对象,将对象初始化为一个特定的初始状态。构造函数有如下特征:
-
函数名与类名相同;
-
不能定义返回值类型,也不能有return语句;
-
可以有形式参数,也可以没有形式参数;
-
可以是内联函数;
-
可以重载,即一个类可以拥有多个构造函数;
重载的构造函数必须避免二义性(在形式参数的类型、个数和顺序等至少一个方面不一样)
-
可以带默认参数值。
只要定义了一个对象就势必触发构造函数的调用,每次定义类对象时,编译器会自动查找并匹配最合适的构造函数
-
如果编译器发现程序中未定义构造函数,则将自动生成一个默认构造函数(default constructor)再调用;
-
如果程序中定义了构造函数,则编译器不再生成默认构造函数,只从所有已定义的构造函数中选择合适的来调用执行,若没有合适的就报错(如下);
定义对象时没有提供实际参数则会调用无参构造函数,若此时程序中有构造函数但没有无参构造函数则会报错(如下)
此时需要重载一个无参构造函数就可以通过
#include<iostream>
using namespace std;
class Clock {
int hour, minute, second;
public:
Clock();
Clock(int newH, int newM, int newS);
void setTime(int newH = 0, int newM = 0, int newS = 0);
void showTime() {
cout << hour << ":" << minute << ":" << second << endl;
}
};
/*定义无参构造函数(默认构造函数)*/
//Clock::Clock() :hour(0), minute(0), second(0) {} //构造函数的实现可以采用初始化列表形式
Clock::Clock() {
hour = minute = second = 0;
}
/*定义构造函数*/
//Clock::Clock(int newH, int newM, int newS) : hour(newH), minute(newM), second(newS) {} //构造函数的实现可以采用初始化列表形式
Clock::Clock(int newH, int newM, int newS) {
hour = newH;
minute = newM;
second = newS;
}
void Clock::setTime(int newH, int newM, int newS) {
hour = newH;
minute = newM;
second = newS;
}
int main() {
Clock myclock_1, //自动调用无参构造函数
myclock_2(20,13,14); //自动调用有3个形参的构造函数
cout << "myclock_1:" << endl;
myclock_1.showTime();
cout << "myclock_2:" << endl;
myclock_2.showTime();
return 0;
}
(1)初始化列表
若构造函数体中只使用赋值语句初始化对象的数据成员,则还可以用初始化列表的方式达到同样的效果
/*一般形式*/
Clock::Clock(int newH, int newM, int newS) {
hour = newH;
minute = newM;
second = newS;
}
/*初始化列表的形式*/
Clock::Clock(int newH, int newM, int newS) : hour(newH), minute(newM), second(newS) {} //构造函数的实现可以采用初始化列表形式
(2)默认构造函数(Default Constructor)
定义:调用时不需要提供实参的构造函数是默认构造函数
注意:默认构造函数不是因为编译器自动生成所以称其为默认构造函数
默认构造函数有如下特征:
- 参数列表为空,只负责为对象的数据成员分配空间,而不为数据成员设置初始值;
- 如果类内定义了成员的初始值,则使用类内定义的初始值;如果没有定义类内的初始值,则以默认方式初始化;
- 基本类型的数据默认初始化的值是不确定的;
以下两种形式的构造函数都是默认构造函数(两种形式不可以同时出现在一个类中,因为有二义性)
/*形式一:不带形参*/
Clock(){
}
/*形式二:已经提供了(默认)实参*/
Clock(int newH=0,int newM=0,int newS=0){
}
/*不是默认构造函数*/
Clock(int newH, int newM, int newS) {
hour = newH;
minute = newM;
second = newS;
}
例题:对于默认构造函数,下面哪一种说法是错误的?
A. 一个无参构造函数是默认构造函数
B. 只有当类中没有显式定义任何构造函数时,编译器才自动生成一个公有的默认构造函数
C. 默认构造函数一定是一个无参构造函数
D. 一个类中最多只能有一个默认构造函数
本题选C。A选项,无参构造函数一定是默认构造函数,正确。B选项,当类中没有显式定义任何构造函数时,编译器自动生成一个公有的默认构造函数。如果一个类显式地声明了任何构造函数,编译器不生成公有的默认构造函数。在这种情况下,如果程序需要一个默认构造函数,需要由类的设计者提供。正确。C选项,无参构造函数一定是默认构造函数, 而默认构造函数可能是无参构造函数,也可能是所有参数都有默认值的构造函数。因此C选项错误。D选项,一个类只能有一个默认构造函数,一般选择 testClass(); 这种形式的默认构造函数 ,因此D选项描述正确。综上,默认构造函数可能是无参构造函数(形式一),也可能是所有参数都有默认值的构造函数(形式二),因此C选项错误。本题选C。
(3)委托构造函数
即允许构造函数通过初始化列表调用同一个类的其他构造函数,目的是简化构造函数的书写,提高代码的可维护性,避免代码冗余膨胀。
举个例子
/*定义构造函数*/
Clock::Clock(int newH, int newM, int newS) : hour(newH), minute(newM), second(newS) {} //构造函数的实现可以采用初始化列表形式
/*定义无参构造函数(默认构造函数)*/
Clock::Clock() :hour(0), minute(0), second(0) {} //构造函数的实现可以采用初始化列表形式
用委托构造函数后可以将上面的写成
/*定义构造函数*/
Clock::Clock(int newH, int newM, int newS) : hour(newH), minute(newM), second(newS) {} //构造函数的实现可以采用初始化列表形式
/*委托构造函数*/
Clock::Clock() :Clock(0,0,0) {}
(4)复制构造函数(Copy Constructor)
复制构造函数是一种重载的构造函数, 其形参为本类的对象引用,可以帮助我们用一个已经存在(已被定义)的对象去初始化新的同类型对象。
/*函数原型*/
类名(const 类名 &对象名); //参数是本类对象的(常)引用
下面3种情况下系统自动调用复制构造函数:
-
明确表示由一个已定义的对象初始化一个新对象;
注意:这里是初始化!!并不是赋值!!
-
如果函数的形参是类的对象(不是对象的引用),调用函数时,将使用实参对象初始化形参对象,发生复制构造;
为了避免因形参为类的对象而调用复制构造函数造成额外开销,通常会将形参设置成对象的引用
-
如果函数的返回值是类的对象,函数执行完成返回主调函数时,将使用return语句中的对象初始化一个临时无名对象, 传递给主调函数,此时发生复制构造。
很多编译器会认为此情况多余而将其优化掉
若程序中没有定义复制构造函数,则系统会自动隐含生成默认复制构造函数并实现对应数据成员一一复制,自定义的复制构造函数可以根据需要实现特殊的复制功能。
#include<iostream>
using namespace std;
class Clock {
int hour, minute, second;
public:
Clock(int newH = 0, int newM = 0, int newS = 0); //构造函数
Clock(const Clock &time); //复制构造函数,参数是本类对象的常引用
void showTime() { //显示时间
cout << hour << ":" << minute << ":" << second << endl;
}
};
/*构造函数*/
Clock::Clock(int newH, int newM, int newS) {
hour = newH;
minute = newM;
second = newS;
cout << "Constructor called" << endl;
}
/*复制构造函数*/
Clock::Clock(const Clock &time) {
hour = time.hour; //类的成员函数内部可以访问任何同类对象的私有成员
minute = time.minute;
second = time.second;
cout << "Copy Constructor called." << endl;
}
/*普通函数,用于创建新对象并返回*/
Clock fun(Clock time) {
Clock newtime(time); //调用复制构造函数(情形1)
return newtime; //调用赋值构造函数(情形3)
}
int main() {
Clock time1(20, 13, 14), //调用普通构造函数
time3, //调用普通构造函数
time2(time1), //调用复制构造函数(情形1)
time4=time2; //调用复制构造函数(情形1),等效于"Clock time4(time2);"
time3 = time2; //赋值语句,涉及的对象都是已经初始化过的,所以不调用赋值构造函数
time3 = fun(time2); //调用赋值构造函数(情形2)
time3.showTime();
return 0;
}
/*输出:
Constructor called
Constructor called
Copy Constructor called.
Copy Constructor called.
Copy Constructor called.
Copy Constructor called.
Copy Constructor called.
20:13:14
*/
注意:赋值运算与复制构造没有任何关系,复制构造是用于初始化的
禁用复制构造函数
有时我们不希望对象被复制构造
class Clock {
int hour, minute, second;
public:
... ...
Clock(const Clock &time)=delete; //禁用复制构造函数
... ...
};
此时若再出现触发调用复制构造函数的那3种情形会报错
(5)移动构造函数
2. 析构函数
3. 组合类的构造函数设计
A类的一个对象作为B类的数据成员,则该对象为对象成员,B类为组合类,且构造函数的通用形式如下:
组合类名::组合类名(对象成员所需的形参,本类基本类型数据成员形参):内嵌对象1(参数),内嵌对象2(参数),....{
/*函数体其它语句*/
}
组合类的构造函数中代码执行顺序如下:
- 首先对构造函数初始化列表中列出的成员(包括基本类型成员和对象成员)进行初始化,初始化次序是成员在类体中定义的次序
- 对象成员的构造函数的调用顺序:按对象成员的声明顺序,先声明的先构造。
- 初始化列表中未出现的成员对象,调用用默认构造函数(即无形参的)初始化
- 处理完初始化列表之后,再执行构造函数的函数体。
/*测试初始化顺序*/
#include<iostream>
using namespace std;
class Point1 {
double x, y;
public:
Point1(double newx = 0, double newy = 0);
};
class Point2 {
double x, y;
public:
Point2(double newx = 0, double newy = 0);
};
class Line {
Point1 p1;
Point2 p2;
public:
Line(double x_1, double x_2, double y_1, double y_2);
};
Point1::Point1(double newx, double newy) :x(newx), y(newy) {
cout << "Class Point1's constructor called." << endl;
}
Point2::Point2(double newx, double newy) : x(newx), y(newy) {
cout << "Class Point2's constructor called." << endl;
}
Line::Line(double x_1, double x_2, double y_1, double y_2) :p2(x_2, y_2), p1(x_1, y_1) {
cout << "Class Line's constructor called." << endl;
}
int main() {
Line line(3, 4, 5, 6);
return 0;
}
/*输出:
Class Point1's constructor called.
Class Point2's constructor called.
Class Line's constructor called.
*/
/*测试构造函数调用顺序*/
#include<iostream>
#include<cmath>
using namespace std;
/*点类*/
class Point {
double x, y;
public:
Point(double newx = 0, double newy = 0);
Point(const Point &point);
void setPoint(double newx, double newy);
void showPointLoc();
double getX();
double getY();
};
/*线段类(组合类)*/
class Line {
Point p1, p2;
double len;
public:
Line(double x_1, double x_2, double y_1, double y_2);
Line(Point newp1, Point newp2);
Line(const Line &line);
double getLen();
};
Point::Point(double newx, double newy) {
x = newx;
y = newy;
cout << "Class Point's constructor called." << endl;
}
Point::Point(const Point &point) {
//x = point.getX(); //错误,常对象不能调用普通的成员函数
//y = point.getY(); //错误,常对象不能调用普通的成员函数
x = point.x;
y = point.y;
cout << "Class Point's copy constructor called." << endl;
}
void Point::setPoint(double newx, double newy) {
x = newx;
y = newy;
}
double Point::getX() {
return x;
}
double Point::getY() {
return y;
}
void Point::showPointLoc() {
cout << "(" << x << "," << y << ")" << endl;
}
/*组合类的构造函数(2种形式)*/
Line::Line(double x_1, double x_2, double y_1, double y_2) :p1(x_1, y_1), p2(x_2, y_2) {
double a = x_1 - x_2,
b = y_1 - y_2;
len = a * a + b * b;
cout << "Class Line's constructor_1 called." << endl;
}
Line::Line(Point newp1, Point newp2) :p1(newp1), p2(newp2) {
double a = newp1.getX() - newp2.getX(),
b = newp2.getY() - newp2.getY();
/*错误,类的成员函数内部可以访问任何同类对象的私有成员,但这里是Line类内部,不能访问Point类对象私有成员*/
//double a = newp1.x - newp2.x,
// b = newp2.y - newp2.y;
len = sqrt(a * a + b * b);
cout << "Class Line's constructor_2 called." << endl;
}
/*组合类的复制构造函数*/
Line::Line(const Line &line) :p1(line.p1), p2(line.p2) { //类的成员函数内部可以访问任何同类对象的私有成员
cout << "Class Line's copy constructor called." << endl;
len = line.len;
}
double Line::getLen() {
return len;
}
int main() {
Point p1(7, 8);
Point p2(9, 10);
cout << "-------------" << endl;
Line l1(3, 4, 5, 6);
cout << "-------------" << endl;
Line l2(p1, p2);
cout << "-------------" << endl;
Line l3(l2);
cout << "-------------" << endl;
return 0;
}
/*输出:
Class Point's constructor called.
Class Point's constructor called.
-------------
Class Point's constructor called.
Class Point's constructor called.
Class Line's constructor_1 called.
-------------
Class Point's copy constructor called.
Class Point's copy constructor called.
Class Point's copy constructor called.
Class Point's copy constructor called.
Class Line's constructor_2 called.
-------------
Class Point's copy constructor called.
Class Point's copy constructor called.
Class Line's copy constructor called.
-------------
*/
4. 前向引用声明
C++规定类应该先声明,后使用,但如果需要在某个类的声明之前,引用该类,则应进行前向引用声明,前向引用声明只为程序引入一个标识符,但具体声明在其他地方。
举例如下:
class B; //前向引用声明
class A{
public:
void fun1(B b);
};
class B{
public:
void fun2(A a);
};
即便是使用前向引用声明,在提供一个完整的类声明之前,不能声明该类的对象,也不能在内联成员函数中使用该类的对象。
class B; //前向引用声明
class A{
B b; //错误,因为定义A类需要知道其成员的全部细节,但B的细节此时不知道
};
class B{
A a;
};
(四)this指针
当定义了一个类的若干对象,这些对象显然共享所属类的成员函数代码,当成员函数需要获取对象的数据成员时,如何精准找到这个对象的数据成员的内存空间?this指针便可以解决这个问题。
this 指针实际上是成员函数的一个形参,在调用非静态成员函数时将对象的地址作为实参传递给 this(静态成员函数没有this),不过 this 这个形参是隐式的,它并不出现在代码中,而是在编译阶段由编译器默默地将它添加到参数列表中,对象调用成员函数时 this 被赋值为当前对象的地址。其实类似于Python的类方法第一个参数——self
。
以下面代码为例,若程序定义了Point
类对象A和B,那么A在调用showPointLoc
时,它的地址便被记录在this
指针中,这样函数输出x
时输出的其实是this->x
,即A的x
,而不是B的x
。
void Point::showPointLoc() {
cout << "(" << x << "," << y << ")" << endl;
}
(七)cin和cout
cout是一个ostream 类对象,ostream 类在iostream 文件中定义并描述了ostream 对象表示的数据以及可以对它执行的操作,如将数字或字符串插入到输出流中。cin 是一个istream类对象,也是在iostream中定义的。
1. 控制符
① endl
控制符endl
用于指示cout重起一行。
② dec/hex/oct
控制符dec
、hex
和oct
分别用于指示cout以十进制、十六进制和八进制格式显示整数。
#include<iostream>
using namespace std;
int main() {
int num{ 66 };
cout << "(十进制)" << num << " = ";
cout << hex;
cout << "(16进制)" << num << " = ";
cout << oct;
cout << "(8进制)" << num;
return 0;
}
/*输出:
(十进制)66 = (16进制)42 = (8进制)102
*/
2. cin>>
功能:根据后面变量的类型读取数据。
原理:一开始,缓冲区为空,cin的成员函数会阻塞等待数据的到来,用户从键盘输入字符串,输入完毕后敲一下回车键代表本次输入结束,其实一次键盘输入并不是直接赋给变量,而是存放到输入缓冲区(按下回车之后字符串被送入到缓冲区中),之后cin的成员函数开始从输入缓冲区读取值,若缓冲区中第一个字符就是空格、tab或换行这些分隔符时,cin>>会将其视作无效字符忽略并清除,继续读取下一个字符,连续读取有效字符直至遇到空格、tab、换行这些分隔符后将已经读取的值赋给变量并在缓冲区中将其清空,但字符后面的分隔符是残留在缓冲区的,不做处理。
输入完毕后敲击的回车键
\r
也会被转换为一个换行符\n
存储在输入缓冲区中,比如我们在键盘上敲下了123456
这个字符串,然后敲一下回车键\r
将这个字符串送入了缓冲区中,那么此时缓冲区中的字节个数是7 ,而不是6。
3. cin.ignore
cin.ignore(int n=1,char delim=EOF)
函数表示从输入流中提取字符,提取的字符被忽略不被使用。每抛弃一个字符,它都要计数并将其与delim比较,如果计数值达到 n (已经抛弃了n个字符)或者被抛弃的字符是 delim,则 cin.ignore() 函数执行终止;否则,它继续等待。
2个参数都有默认值,因此 cin.ignore() 就等效于 cin.ignore(1, EOF), 即跳过一个字符。
该函数常用于跳过输入中的无用部分,以便提取有用的部分。
#include<iostream>
using namespace std;
int main() {
int n;
cin.ignore(5, 'A');
cin >> n;
cout << n;
return 0;
}
- 当输入
abcde34↙
时会输出34
,这是因为cin.ignore() 跳过了输入中的前 5 个字符,其余内容被当作整数输入 n 中。 - 当输入
abA34↙
时会输出34
,这是因为cin.ignore() 跳过了输入中的 'A' 及其前面的字符,其余内容被当作整数输入 n 中。
4. cout.put
cout.put(ch)
-
参数:参数
ch
可以是字符也可以是对应的ASCII值 -
功能:输出单个字符
ch
; -
返回值:返回一个 ostream 类的引用对象,可以理解为返回的是 cout 的引用。
#include<iostream>
using namespace std;
int main() {
char ch = 's';
cout.put(69).put('m').put('0').put(ch);
return 0;
}
/*输出:
Em0s
*/
5. cout.setf¶
\[\tag{1}fmtflags\enspace setf(fmtflags\enspace flags); \]\[\tag{2}fmtflags\enspace setf( fmtflags\enspace flags, fmtflags\enspace mask ); \]通过设置格式标志来控制输出形式实现格式打印输出,有些flag直接用形式(1)就可以实现,但有的需要与相应的mask搭配采用形式(2)。
常用标志(flag) | 含义 |
---|---|
boolalpha | 可以使用单词”true”和”false”进行输入/输出的布尔值. |
oct | 用八进制格式显示数值. |
dec | 用十进制格式显示数值. |
hex | 用十六进制格式显示数值. |
left | 输出调整为左对齐. |
right | 输出调整为右对齐. |
internal | 输出调整为居中对齐(将填充字符填充在符号和数值之间.) |
scientific | 用科学记数法显示浮点数. |
fixed | 用正常的记数方法显示浮点数(与科学计数法相对应). |
showbase | 输出时显示所有数值的基数. |
showpoint | 显示小数点和额外的零(即使0可省) |
showpos | 在非负数值前面显示“+”(正号). |
skipws | 当从一个流进行读取时,跳过空白字符(spaces, tabs, newlines). |
unitbuf | 在每次插入以后,清空缓冲区. |
uppercase | 以大写的形式显示科学记数法中的“e”和十六进制格式的“x”. |
mask | 含义 |
---|---|
basefield | 以多少进制显示。对应可选flag有dec 、oct 、hex |
adjustfield | 对齐方式。对应可选flag有left 、right 、internal |
floatfield | 以什么形式显示浮点数。对应可选flag有scientific 、fixed |
6. setprecision¶
十一、数据共享与保护
(一)函数间数据共享
1. 变量的作用域
(1)函数原型作用域
变量:函数原型中的形参
范围:函数原型中的参数作用域始于(
,终于)
。
double area(double radius); //正确,函数原型内的形参变量名称可以舍去。
double area(double); //正确
(2)局部作用域
变量:函数的形参、在块中声明的标识符
范围:自声明处起,限于块中
(3)类作用域
变量:类的成员
范围:类体+非内联函数函数体
如果在类作用域以外访问类的成员,要通过类名(访问静态成员时)或者该类的对象名、对象引用、对象指针(访问非静态成员时)
(4)命名空间作用域
① 命名空间
程序访问名称空间std
的方法可以有如下几种:
- 将
using namespace std;
放在函数定义之前,告知编译器后续的代码将使用std
命名空间中的名称。 - 将
using namespace std;
放在特定的函数定义中,让该函数能够使用名称空间std
中的所有元素。 - 在特定的函数中使用类似
using std::cout;
这样的编译指令,而不是using namespace std;
,让该函数能够使用指定的元素,如cout
。 - 完全不使用编译指令
using
,而在需要使用名称空间std
中的元素时,使用前缀std::
,如下所示std::cout << "Em0s_Er1t" << std::endl;
用户可以自定义命名空间来解决重名问题
#include <iostream>
using namespace std;
/*第1个命名空间*/
namespace first_space{
void func(){
cout << "Inside first_space" << endl;
}
}
/*第2个命名空间*/
namespace second_space{
void func(){
cout << "Inside second_space" << endl;
}
}
using namespace first_space;
int main ()
{
func(); // 调用第1个命名空间中的函数
return 0;
}
/*输出:
Inside first_space
*/
(5)枚举类作用域
见枚举类
2. 同名变量与可见性
可见性表示从内层作用域向外层作用域“看”时能看见什么。如果标识在某处可见,就可以在该处引用此标识符。
- 如果某个标识符在外层中声明,且在内层中没有同一标识符的声明,则该标识符在内层可见;
- 对于两个嵌套的作用域,如果在内层作用域内声明了与外层作用域中同名的标识符,则外层作用域的标识符在内层不可见。(即内层屏蔽外层同名的标识符)
#include<iostream>
using namespace std;
int i = 1;
int main() {
int i = 2;
{
int i = 3;
cout << i << endl; //输出3
cout << ::i << endl; //输出1(指定全局变量i)
}
cout << i << endl; //输出2
return 0;
}
3. 变量的生存期
生存期 :即从诞生到消失的时间段,在生存期内,对象的值保持不变,直到被改变为止。
生存期可以分为静态生存期与动态生存期
-
静态生存期与程序运行期相同,在函数内部声明静态生存期对象要冠以关键字
static
,如静态局部变量全局变量具有静态生存期
-
动态生存期是在块作用域中声明的,没有用static修饰的
(1)静态变量
注意:若一个静态变量未经过初始化,则其自动被初始化为0
① 静态局部变量
- 生存期:整个源程序运行期间
- 作用域:与局部变量相同,只能在定义该变量的函数内使用该变量。
② 静态全局变量
-
生存期:整个源程序运行期间
-
作用域:全局作用域
静态全局变量与全局变量的作用域
- 静态全局变量作用于定义它的文件里,不能作用到其它文件里,因此即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。
- 全局变量作用于整个工程
#include<iostream>
using namespace std;
int i = 1;
void other();
int main() {
static int a;
int b = -10, c = 0;
cout << "-main-" << endl;
cout << "i:" << i << endl;
cout << "a:" << a << endl;
cout << "b:" << b << endl;
cout << "c:" << c << endl;
c += 8;
cout << "第1次调用other" << endl;
other();
cout << "第2次调用other" << endl;
i += 10;
other();
cout << "-main-" << endl;
return 0;
}
void other(){
static int a = 2, b;
int c = 10;
a += 2;
i += 32;
c += 5;
cout << "i:" << i << endl;
cout << "a:" << a << endl;
cout << "b:" << b << endl;
cout << "c:" << c << endl;
b = a;
}
/*输出:
-main-
i:1
a:0
b:-10
c:0
第1次调用other
i:33
a:4
b:0
c:15
第2次调用other
i:75
a:6
b:4
c:15
-main-
*/
(二)对象间的数据共享
1. 同类对象间数据共享:静态数据成员
静态数据成员在类内用关键字static
声明,为该类的所有对象共享。
-
初始化:静态数据成员在类外定义与初始化并用类名限定(必须初始化!!!)
C++11支持静态常量(const或constexpr修饰)类内初始化,此时类外仍可定义该静态成员,但不可再次初始化操作。
class A{ ...... static int a; //(类内)静态数据成员声明 ...... } int A::a = 0; //(类外)静态数据成员定义与初始化
-
生存期:整个源程序运行期间(即静态数据成员具有静态生存期)
2. 同类对象功能共享:静态函数成员
静态函数成员不属于任何一个对象,它主要用于处理该类的静态数据成员。
-
类外可以直接使用类名和作用域操作符调用静态成员函数(静态成员函数无this指针),也可以使用已定义的对象来调用。
-
静态成员函数无法直接访问非静态数据成员。
因为静态成员函数被调用时可能一个对象还没创建,自然也无法访问非静态数据成员
#include<iostream>
using namespace std;
class Point {
int x, y;
static int count; //给创建的点计数
public:
Point(int newx = 0, int newy = 0);
Point(const Point &p);
~Point();
int getx();
int gety();
static void showcount();
};
int Point::count = 0; //静态数据成员在类外定义与初始化,并用类名限定
Point::Point(int newx, int newy) :x(newx), y(newy) {
count++;
}
Point::Point(const Point &p) : x(p.x), y(p.y) {
count++;
}
Point::~Point() {
count--;
}
int Point::getx() {
return x;
}
int Point::gety() {
return y;
}
void Point::showcount() { //静态成员函数的定义前面无需加static
cout << "now you have " << count << " points." << endl;
}
int main() {
Point::showcount();
Point p(1, 2);
p.showcount(); //即便用特定的对象去调用静态成员函数也不会传入该对象的地址
return 0;
}
3. 类与外部数据共享:友元
(1)友元类
类的成员函数内部可以访问任何同类对象的私有成员,对于非本类对象是无法访问其私有成员的,但如果声明B类是A类的友元(如下),则B类的成员函数就可以访问A类的私有和保护数据,但A类的成员函数却不能访问B类的私有、保护数据。(友元关系是单向的)
class A{
friend class B; //A声明B类是A类的友元
private:
...
public:
...
}
(2)友元函数
#include<iostream>
#include<cmath>
using namespace std;
class Point {
friend double getdistance(Point &p1, Point &p2); //不是类的成员,只是一个声明,可以放置在大括号内的任意位置
int x, y;
public:
Point(int newx = 0, int newy = 0);
Point(const Point &p);
};
Point::Point(int newx, int newy) :x(newx), y(newy) {
}
Point::Point(const Point &p) : x(p.x), y(p.y) {
}
double getdistance(Point &p1, Point &p2);
int main() {
Point p1(3, 4), p2(1, 2);
cout << "the distance of the 2 points is " << getdistance(p1, p2) << endl;
return 0;
}
double getdistance(Point &p1, Point &p2) {
int a = p1.x - p2.x, //此时在友元函数中可以直接访问该类的私有成员
b = p1.y - p2.y;
double d = sqrt(a*a + b * b);
return d;
}
/*输出:
the distance of the 2 points is 2.82843
*/
(三)共享数据的保护:const
对于既需要共享、又需要防止改变的数据应该声明为常类型(用const进行修饰)
-
常对象:必须进行初始化,不能被更新
const 类名 对象名
注意:通过常对象只能调用它的常成员函数
-
常成员:用const修饰的常数据成员和常函数成员
-
常引用:被引用的对象不能被更新
const 类型名 &引用名称
-
常数组:数组元素不能被更新
类型名 const 数组名[数组大小]
-
常指针:指向常量的指针
对于不改变对象状态的成员函数应该声明为常函数。
1. 常成员
(1)常成员函数
类型说明符 函数名(参数表) const;
注意:
- 常成员函数不能更新对象的非静态数据成员(如果有此类操作则编译器编译时报错),但可以更新静态数据成员;(可以理解为静态成员不严格属于类成员)
- 常成员函数不能调用非静态成员函数(如果有此类操作则编译器编译时报错),但可以调用静态成员函数(可以理解为静态成员不严格属于类成员)。
- const是函数类型的组成部分,因此在实现部分也要带const关键字
- const关键字可以用于参与对重载函数的区分
- 实际应用:通过在类中定义常成员函数,类的常对象就可以调用这些函数实现一些功能(如打印数据成员的值),否则常对象无法实现一些功能
(2)常数据成员
- 初始化:常数据成员只能通过初始化列表来获得初值
2. 常引用
如果在声明弓|用时用const修饰,被声明的引用就是常引用。
const 类型名 &引用名称
注意:
- 常引用所引用的对象不能被更新。
- 实际应用:用常引用做形参便不会意外地发生对实参的更改,且不会触发复制构造函数的调用。
易错点
- 常函数成员中不能更新对象的非静态数据成员(如果有此类操作则编译器编译时报错),但可以更新静态数据成员;不能调用非静态成员函数,但可以调用静态成员函数。(可以理解为静态成员不严格属于类成员)