- 项目开发需要使用 C++,曾经学过相关课程,但很久没与使用,有些遗忘。
- 此处将和自己比较擅长的 Java 语言其中的概念进行类比,加深理解。
- 记录重要笔记,主要是C++ 一些独有的概念和一些基础知识
- 前半部分是 C 和 C++ 共通的部分,后半部分为面向对象的相关特性。
参考链接
- https://www.runoob.com/cplusplus/cpp-tutorial.html
基本概念
标准库组成
- 核心语言,提供了所有构件块,包括变量、数据类型和常量,等等。
- C++ 标准库,提供了大量的函数,用于操作文件、字符串等。
- 标准模板库(STL),提供了大量的方法,用于操作数据结构等。
环境搭建
- 参考百度/Google
基本语法
- 面向对象编程语言的基本概念。类、对象、方法(函数)
- 命名空间 namespace
数据类型
- bool
- char = '\0'
- int = 0
- float = 0
- double = 0
- void
- wchar_t 宽字符型 typedef short int wchar_t;
- 枚举、指针、数组、引用、数据结构、类
- pointer = NULL
基本数据类型修饰符
- signed
- unsigned
- short
- long
类型限定符
- const 类型的对象在程序执行期间不能被修改改变。
- volatile 修饰符 volatile 告诉编译器不需要优化volatile声明的变量,让程序可以直接从内存中读取变量。对于一般的变量编译器会对变量进行优化,将内存中的变量值放在寄存器中以加快读写效率。
- restrict 由 restrict 修饰的指针是唯一一种访问它所指向的对象的方式。只有 C99 增加了新的类型限定符 restrict。
typedef
- 为一个已有的类型取一个新的名字:
typedef int feet;
feet distance;
枚举类型
- 默认枚举对应数值 0,1,2;也可以指定
enum color { red, green, blue } c;
c = blue;
enum color { red, green=5, blue };
变量
- extern 外部引用关键字
extern int d = 3, f = 5; // d 和 f 的声明,引入其他文件中定义的变量
int d = 3, f = 5; // 定义并初始化 d 和 f
byte z = 22; // 定义并初始化 z
char x = 'x'; // 变量 x 的值为 'x'
声明
#include <iostream>
using namespace std;
// 变量声明
extern int a, b;
extern int c;
extern float f;
// 函数声明
int func();
int main ()
{
// 变量定义
int a, b;
int c;
float f;
// 实际初始化
a = 10;
b = 20;
c = a + b;
cout << c << endl ;
f = 70.0/3.0;
cout << f << endl ;
// 函数调用
int i = func();
return 0;
}
// 函数定义
int func()
{
return 0;
}
作用域
- 在函数或一个代码块内部声明的变量,称为局部变量。
- 在函数参数的定义中声明的变量,称为形式参数。
- 在所有函数外部声明的变量,称为全局变量。
#include <iostream>
using namespace std;
// 全局变量声明
int g;
int main ()
{
// 局部变量声明
int a, b;
int c;
// 实际初始化
a = 10;
b = 20;
c = a + b;
g = a + b;
cout << c;
cout << g;
return 0;
}
- 在程序中,局部变量和全局变量的名称可以相同,但是在函数内,局部变量的值会覆盖全局变量的值
常量
定义常量
- #define 域处理器
#include <iostream>
using namespace std;
#define LENGTH 10
#define WIDTH 5
#define NEWLINE '\n'
int main()
{
int area;
area = LENGTH * WIDTH;
cout << area;
cout << NEWLINE;
return 0;
}
- const 关键字
#include <iostream>
using namespace std;
int main()
{
const int LENGTH = 10;
const int WIDTH = 5;
const char NEWLINE = '\n';
int area;
area = LENGTH * WIDTH;
cout << area;
cout << NEWLINE;
return 0;
}
存储类
auto (C++17 弃用)
- 声明变量时根据初始化表达式自动推断该变量的类型、声明函数时函数返回值的占位符。
auto f=3.14; //double
auto s("hello"); //const char*
auto z = new auto(9); // int*
auto x1 = 5, x2 = 5.0, x3='r';//错误,必须是初始化为同一类型
register (C++17 弃用)
- 用于定义存储在寄存器中而不是 RAM 中的局部变量。这意味着变量的最大尺寸等于寄存器的大小(通常是一个字),且不能对它应用一元的 '&' 运算符(因为它没有内存位置)。
static
- 指示编译器在程序的生命周期内保持局部变量的存在,而不需要在每次它进入和离开作用域时进行创建和销毁。因此,使用 static 修饰局部变量可以在函数调用之间保持局部变量的值。也可以应用于全局变量。当 static 修饰全局变量时,会使变量的作用域限制在声明它的文件内。
#include <iostream>
// 函数声明
void func(void);
static int count = 10; /* 全局变量 */
int main()
{
while(count--)
{
func();
}
return 0;
}
// 函数定义
void func( void )
{
static int i = 5; // 局部静态变量
i++;
std::cout << "变量 i 为 " << i ;
std::cout << " , 变量 count 为 " << count << std::endl;
}
变量 i 为 6 , 变量 count 为 9
变量 i 为 7 , 变量 count 为 8
变量 i 为 8 , 变量 count 为 7
变量 i 为 9 , 变量 count 为 6
变量 i 为 10 , 变量 count 为 5
变量 i 为 11 , 变量 count 为 4
变量 i 为 12 , 变量 count 为 3
变量 i 为 13 , 变量 count 为 2
变量 i 为 14 , 变量 count 为 1
变量 i 为 15 , 变量 count 为 0
inline
- C++ 内联函数是通常与类一起使用。如果一个函数是内联的,那么在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方。
- 对内联函数进行任何修改,都需要重新编译函数的所有客户端,因为编译器需要重新更换一次所有的代码,否则将会继续使用旧的函数。
- 内联函数inline:引入内联函数的目的是为了解决程序中函数调用的效率问题,这么说吧,程序在编译器编译的时候,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体进行替换,而对于其他的函数,都是在运行时候才被替代。这其实就是个空间代价换时间的节省。所以内联函数一般都是1-5行的小函数。
- 注意事项:
- 1.在内联函数内不允许使用循环语句和开关语句、静态变量、递归
- 2.内联函数的定义必须出现在内联函数第一次调用之前;
#include <iostream>
using namespace std;
inline int Max(int x, int y)
{
return (x > y)? x : y;
}
// 程序的主函数
int main( )
{
cout << "Max (20,10): " << Max(20,10) << endl;
cout << "Max (0,200): " << Max(0,200) << endl;
cout << "Max (100,1010): " << Max(100,1010) << endl;
return 0;
}
//运行结果
Max (20,10): 20
Max (0,200): 200
Max (100,1010): 1010
extern
- 用于提供一个全局变量的引用,全局变量对所有的程序文件都是可见的。当您使用 'extern' 时,对于无法初始化的变量,会把变量名指向一个之前定义过的存储位置。extern 是用来在另一个文件中声明一个全局变量或函数。
// main.cpp
#include <iostream>
int count ;
extern void write_extern();
int main()
{
count = 5;
write_extern();
}
// suport.cpp
#include <iostream>
extern int count;
void write_extern(void)
{
std::cout << "Count is " << count << std::endl;
}
- 在这里,第二个文件中的 extern 关键字用于声明已经在第一个文件 main.cpp 中定义的 count。
- 现在 ,编译这两个文件,如下所示:
$ g++ main.cpp support.cpp -o write
mutable
- 说明符仅适用于类的对象。它允许对象的成员替代常量。也就是说,mutable 成员可以通过 const 成员函数修改。
thread_local (C++11)
- 使用 thread_local 说明符声明的变量仅可在它在其上创建的线程上访问。 变量在创建线程时创建,并在销毁线程时销毁。 每个线程都有其自己的变量副本。
- thread_local 说明符可以与 static 或 extern 合并。
- 可以将 thread_local 仅应用于数据声明和定义,thread_local 不能用于函数声明或定义。
thread_local int x; // 命名空间下的全局变量
class X
{
static thread_local std::string s; // 类的static成员变量
};
static thread_local std::string X::s; // X::s 是需要定义的
void foo()
{
thread_local std::vector<int> v; // 本地变量
}
运算符 和 语句
- 此处不表,只是需要注意指针的相关运算符,->, *, &
- 循环语句、条件语句同理
函数
- 函数定义/声明/调用 此处不表
函数参数
- 传值/指针/引用 与 Java 区分 (Java 只有值传递和引用传递)
- 传值:该方法把参数的实际值赋值给函数的形式参数。在这种情况下,修改函数内的形式参数对实际参数没有影响。
- 指针:该方法把参数的地址赋值给形式参数。在函数内,该地址用于访问调用中要用到的实际参数。这意味着,修改形式参数会影响实际参数。
- 引用:该方法把参数的引用赋值给形式参数。在函数内,该引用用于访问调用中要用到的实际参数。这意味着,修改形式参数会影响实际参数。
函数参数默认值
- 定义一个函数,您可以为参数列表中后边的每一个参数指定默认值。当调用函数时,如果实际参数的值留空,则使用这个默认值。
int sum(int a, int b=20)
lamda
- C++11 提供了对匿名函数的支持,称为 Lambda 函数(也叫 Lambda 表达式)。
- Lambda 表达式把函数看作对象。Lambda 表达式可以像对象一样使用,比如可以将它们赋给变量和作为参数传递,还可以像函数一样对其求值。
- Lambda 表达式本质上与函数声明非常类似。
// [capture](parameters)->return-type{body}
[](int x, int y){ return x < y ; }
// [capture](parameters){body}
[]{ ++global_x; }
[](int x, int y) -> int { int z = x + y; return z + x; }
[] // 沒有定义任何变量。使用未定义变量会引发错误。
[x, &y] // x以传值方式传入(默认),y以引用方式传入。
[&] // 任何被使用到的外部变量都隐式地以引用方式加以引用。
[=] // 任何被使用到的外部变量都隐式地以传值方式加以引用。
[&, x] // x显式地以传值方式加以引用。其余变量以引用方式加以引用。
[=, &z] // z显式地以引用方式加以引用。其余变量以传值方式加以引用。
- 另外有一点需要注意。对于[=]或[&]的形式,lambda 表达式可以直接使用 this 指针。但是,对于[]的形式,如果要使用 this 指针,必须显式传入:
[this]() { this->someFunc(); }();
数字
double cos(double);
double sin(double);
double tan(double);
double log(double); // 自然对数
double pow(double, double);
double hypot(double, double); // 两个参数的平方总和的平方根
double sqrt(double);
int abs(int);
double fabs(double);
double floor(double); // 向下取整
随机数
#include <iostream>
#include <ctime>
#include <cstdlib>
using namespace std;
int main ()
{
int i,j;
// 设置种子
srand( (unsigned)time( NULL ) );
/* 生成 10 个随机数 */
for( i = 0; i < 10; i++ )
{
// 生成实际的随机数
j= rand(); // 伪随机
cout <<"随机数: " << j << endl;
}
return 0;
}
数组
- 此处不表,注意在 C/C++ 中和 指针的联系
字符串
C 风格
char greeting[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
char greeting[] = "Hello"
;
cstring
#include <iostream>
#include <cstring>
using namespace std;
int main ()
{
char str1[11] = "Hello";
char str2[11] = "World";
char str3[11];
int len ;
// 复制 str1 到 str3
strcpy( str3, str1);
cout << "strcpy( str3, str1) : " << str3 << endl;
// 连接 str1 和 str2
strcat( str1, str2);
cout << "strcat( str1, str2): " << str1 << endl;
// 连接后,str1 的总长度
len = strlen(str1);
cout << "strlen(str1) : " << len << endl;
return 0;
}
C++
string
#include <iostream>
#include <string>
using namespace std;
int main ()
{
string str1 = "Hello";
string str2 = "World";
string str3;
int len ;
// 复制 str1 到 str3
str3 = str1;
cout << "str3 : " << str3 << endl;
// 连接 str1 和 str2
str3 = str1 + str2;
cout << "str1 + str2 : " << str3 << endl;
// 连接后,str3 的总长度
len = str3.size();
cout << "str3.size() : " << len << endl;
return 0;
}
指针和引用
- 指针和引用的相同点和不同点
- 相同点:
- 都是地址的概念,
- 指针指向一块内存,内容是内存的地址
- 引用则是某块内存的别名
- 都是地址的概念,
- 不同点:
- 指针是一个实体,引用是一个别名
- 引用只能在定义时被初始化一次,之后不可变,指针可变
- 引用没有 const,即无 int& const a; 但指针有 const
- 引用不能为空,指针可以为空
- sizeof(引用) 得到的是所指向的变量(对象)的大小,而 sizeof(指针) 得到的是指针本身的大小
- 指针和引用的自增运算意义不同
- 引用是类型安全的,指针不是,(引用比指针多了类型检查)
- 相同点:
日期/时间
- C++ 标准库没有提供所谓的日期类型。C++ 继承了 C 语言用于日期和时间操作的结构和函数。为了使用日期和时间相关的函数和结构,需要在 C++ 程序中引用
头文件。 - 有四个与时间相关的类型:clock_t、time_t、size_t 和 tm。类型 clock_t、size_t 和 time_t 能够把系统时间和日期表示为某种整数。
struct tm {
int tm_sec; // 秒,正常范围从 0 到 59,但允许至 61
int tm_min; // 分,范围从 0 到 59
int tm_hour; // 小时,范围从 0 到 23
int tm_mday; // 一月中的第几天,范围从 1 到 31
int tm_mon; // 月,范围从 0 到 11
int tm_year; // 自 1900 年起的年数
int tm_wday; // 一周中的第几天,范围从 0 到 6,从星期日算起
int tm_yday; // 一年中的第几天,范围从 0 到 365,从 1 月 1 日算起
int tm_isdst; // 夏令时
}
实例
- 获取当前时间
#include <iostream>
#include <ctime>
using namespace std;
int main( )
{
// 基于当前系统的当前日期/时间
time_t now = time(0);
// 把 now 转换为字符串形式
char* dt = ctime(&now);
cout << "本地日期和时间:" << dt << endl;
// 把 now 转换为 tm 结构
tm *gmtm = gmtime(&now);
dt = asctime(gmtm);
cout << "UTC 日期和时间:"<< dt << endl;
}
- 格式化时间
#include <iostream>
#include <ctime>
using namespace std;
int main( )
{
// 基于当前系统的当前日期/时间
time_t now = time(0);
cout << "1970 到目前经过秒数:" << now << endl;
tm *ltm = localtime(&now);
// 输出 tm 结构的各个组成部分
cout << "年: "<< 1900 + ltm->tm_year << endl;
cout << "月: "<< 1 + ltm->tm_mon<< endl;
cout << "日: "<< ltm->tm_mday << endl;
cout << "时间: "<< ltm->tm_hour << ":";
cout << ltm->tm_min << ":";
cout << ltm->tm_sec << endl;
}
输入/输出
<iostream>
: 该文件定义了 cin、cout、cerr 和 clog 对象,分别对应于标准输入流、标准输出流、非缓冲标准错误流和缓冲标准错误流。<iomanip>
: 该文件通过所谓的参数化的流操纵器(比如 setw 和 setprecision),来声明对执行标准化 I/O 有用的服务。<fstream>
: 该文件为用户控制的文件处理声明服务。我们将在文件和流的相关章节讨论它的细节。
#include <iostream>
#include <iomanip>
using namespace std;
int main()
{
cout<<setiosflags(ios::left|ios::showpoint); // 设左对齐,以一般实数方式显示
cout.precision(5); // 设置除小数点外有五位有效数字
cout<<123.456789<<endl;
cout.width(10); // 设置显示域宽10
cout.fill('*'); // 在显示区域空白处用*填充
cout<<resetiosflags(ios::left); // 清除状态左对齐
cout<<setiosflags(ios::right); // 设置右对齐
cout<<123.456789<<endl;
cout<<setiosflags(ios::left|ios::fixed); // 设左对齐,以固定小数位显示
cout.precision(3); // 设置实数显示三位小数
cout<<999.123456<<endl;
cout<<resetiosflags(ios::left|ios::fixed); //清除状态左对齐和定点格式
cout<<setiosflags(ios::left|ios::scientific); //设置左对齐,以科学技术法显示
cout.precision(3); //设置保留三位小数
cout<<123.45678<<endl;
return 0;
}
结构体
- 直接看实例
// 声明一个结构体类型 Books
#include <iostream>
#include <cstring>
using namespace std;
void printBook( struct Books *book );
struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
};
int main( )
{
Books Book1; // 定义结构体类型 Books 的变量 Book1
Books Book2; // 定义结构体类型 Books 的变量 Book2
// Book1 详述
strcpy( Book1.title, "C++ 教程");
strcpy( Book1.author, "Runoob");
strcpy( Book1.subject, "编程语言");
Book1.book_id = 12345;
// Book2 详述
strcpy( Book2.title, "CSS 教程");
strcpy( Book2.author, "Runoob");
strcpy( Book2.subject, "前端技术");
Book2.book_id = 12346;
// 通过传 Book1 的地址来输出 Book1 信息
printBook( &Book1 );
// 通过传 Book2 的地址来输出 Book2 信息
printBook( &Book2 );
return 0;
}
// 该函数以结构指针作为参数
void printBook( struct Books *book )
{
cout << "书标题 : " << book->title <<endl;
cout << "书作者 : " << book->author <<endl;
cout << "书类目 : " << book->subject <<endl;
cout << "书 ID : " << book->book_id <<endl;
}
面向对象
类和对象
- 基础实例
#include <iostream>
using namespace std;
class Line
{
public:
int getLength( void );
Line( int len ); // 简单的构造函数
Line( const Line &obj); // 拷贝构造函数
~Line(); // 析构函数
private:
int *ptr;
};
// 成员函数定义,包括构造函数
Line::Line(int len)
{
cout << "调用构造函数" << endl;
// 为指针分配内存
ptr = new int;
*ptr = len;
}
Line::Line(const Line &obj)
{
cout << "调用拷贝构造函数并为指针 ptr 分配内存" << endl;
ptr = new int;
*ptr = *obj.ptr; // 拷贝值
}
Line::~Line(void)
{
cout << "释放内存" << endl;
delete ptr;
}
int Line::getLength( void )
{
return *ptr;
}
void display(Line obj)
{
cout << "line 大小 : " << obj.getLength() <<endl;
}
// 程序的主函数
int main( )
{
Line line1(10);
Line line2 = line1; // 这里也调用了拷贝构造函数
display(line1);
display(line2);
return 0;
}
友元函数
- 类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。
- 友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。
- 如果要声明函数为一个类的友元,需要在类定义中该函数原型前使用关键字 friend
#include <iostream>
using namespace std;
class Box
{
double width;
public:
friend void printWidth(Box box);
friend class BigBox;
void setWidth(double wid);
};
class BigBox
{
public :
void Print(int width, Box &box)
{
// BigBox是Box的友元类,它可以直接访问Box类的任何成员
box.setWidth(width);
cout << "Width of box : " << box.width << endl;
}
};
// 成员函数定义
void Box::setWidth(double wid)
{
width = wid;
}
// 请注意:printWidth() 不是任何类的成员函数
void printWidth(Box box)
{
/* 因为 printWidth() 是 Box 的友元,它可以直接访问该类的任何成员 */
cout << "Width of box : " << box.width << endl;
}
// 程序的主函数
int main()
{
Box box;
BigBox big;
// 使用成员函数设置宽度
box.setWidth(10.0);
// 使用友元函数输出宽度
printWidth(box);
// 使用友元类中的方法设置宽度
big.Print(20, box);
getchar();
return 0;
}
内联函数
- C++ 内联函数是通常与类一起使用。如果一个函数是内联的,那么在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方。
- 对内联函数进行任何修改,都需要重新编译函数的所有客户端,因为编译器需要重新更换一次所有的代码,否则将会继续使用旧的函数。
- 如果想把一个函数定义为内联函数,则需要在函数名前面放置关键字 inline,在调用函数之前需要对函数进行定义。如果已定义的函数多于一行,编译器会忽略 inline 限定符。
- 在类定义中的定义的函数都是内联函数,即使没有使用 inline 说明符。
作用
- 引入内联函数的目的是为了解决程序中函数调用的效率问题,这么说吧,程序在编译器编译的时候,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体进行替换,而对于其他的函数,都是在运行时候才被替代。这其实就是个空间代价换时间的i节省。所以内联函数一般都是1-5行的小函数。在使用内联函数时要留神:
- 1.在内联函数内不允许使用循环语句和开关语句;
- 2.内联函数的定义必须出现在内联函数第一次调用之前;
- 3.类结构中所在的类说明内部定义的函数是内联函数。
类中的静态成员
- 使用 static 关键字来把类成员定义为静态的。当我们声明类的成员为静态时,这意味着无论创建多少个类的对象,静态成员都只有一个副本。
- 静态成员在类的所有对象中是共享的。如果不存在其他的初始化语句,在创建第一个对象时,所有的静态数据都会被初始化为零。我们不能把静态成员的初始化放置在类的定义中,但是可以在类的外部通过使用范围解析运算符 :: 来重新声明静态变量从而对它进行初始化。
#include <iostream>
using namespace std;
class Box
{
public:
static int objectCount;
// 构造函数定义
Box(double l=2.0, double b=2.0, double h=2.0)
{
cout <<"Constructor called." << endl;
length = l;
breadth = b;
height = h;
// 每次创建对象时增加 1
objectCount++;
}
double Volume()
{
return length * breadth * height;
}
static int getCount()
{
return objectCount;
}
private:
double length; // 长度
double breadth; // 宽度
double height; // 高度
};
// 初始化类 Box 的静态成员
int Box::objectCount = 0;
int main(void)
{
// 在创建对象之前输出对象的总数
cout << "Inital Stage Count: " << Box::getCount() << endl;
Box Box1(3.3, 1.2, 1.5); // 声明 box1
Box Box2(8.5, 6.0, 2.0); // 声明 box2
// 在创建对象之后输出对象的总数
cout << "Final Stage Count: " << Box::getCount() << endl;
return 0;
}
构造函数
一个类通过定义五种特殊的成员函数来控制对象的拷贝、移动、赋值和销毁操作。
- 拷贝构造函数(copy constructor)
- 拷贝赋值运算符(copy-assignment operator)
- 移动构造函数(move constructor)
- 移动赋值运算符(move-assignment operator)
- 析构函数(destructor)
这些操作统称为拷贝控制操作(copy control)。
在定义任何类时,拷贝控制操作都是必要部分。
拷贝构造函数(The Copy Constructor)
如果一个构造函数的第一个参数是自身类类型的引用(几乎总是const
引用),且任何额外参数都有默认值,则此构造函数是拷贝构造函数。
class Foo
{
public:
Foo(); // default constructor
Foo(const Foo&); // copy constructor
// ...
};
由于拷贝构造函数在一些情况下会被隐式使用,因此通常不会声明为explicit
的。
如果类未定义自己的拷贝构造函数,编译器会为类合成一个。一般情况下,合成拷贝构造函数(synthesized copy constructor)会将其参数的非static
成员逐个拷贝到正在创建的对象中。
class Sales_data
{
public:
// other members and constructors as before
// declaration equivalent to the synthesized copy constructor
Sales_data(const Sales_data&);
private:
std::string bookNo;
int units_sold = 0;
double revenue = 0.0;
};
// equivalent to the copy constructor that would be synthesized for Sales_data
Sales_data::Sales_data(const Sales_data &orig):
bookNo(orig.bookNo), // uses the string copy constructor
units_sold(orig.units_sold), // copies orig.units_sold
revenue(orig.revenue) // copies orig.revenue
{ } // empty bod
使用直接初始化时,实际上是要求编译器按照函数匹配规则来选择与实参最匹配的构造函数。使用拷贝初始化时,是要求编译器将右侧运算对象拷贝到正在创建的对象中,如果需要的话还要进行类型转换。
string dots(10, '.'); // direct initialization
string s(dots); // direct initialization
string s2 = dots; // copy initialization
string null_book = "9-999-99999-9"; // copy initialization
string nines = string(100, '9'); // copy initialization
拷贝初始化通常使用拷贝构造函数来完成。但如果一个类拥有移动构造函数,则拷贝初始化有时会使用移动构造函数而非拷贝构造函数来完成。
发生拷贝初始化的情况:
- 用
=
定义变量。 - 将对象作为实参传递给非引用类型的形参。
- 从返回类型为非引用类型的函数返回对象。
- 用花括号列表初始化数组中的元素或聚合类中的成员。
当传递一个实参或者从函数返回一个值时,不能隐式使用explicit
构造函数。
vector<int> v1(10); // ok: direct initialization
vector<int> v2 = 10; // error: constructor that takes a size is explicit
void f(vector<int>); // f's parameter is copy initialized
f(10); // error: can't use an explicit constructor to copy an argument
f(vector<int>(10)); // ok: directly construct a temporary vector from an int
拷贝赋值运算符(The Copy-Assignment Operator)
重载运算符(overloaded operator)的参数表示运算符的运算对象。
如果一个运算符是成员函数,则其左侧运算对象会绑定到隐式的this
参数上。
赋值运算符通常应该返回一个指向其左侧运算对象的引用。
class Foo
{
public:
Foo& operator=(const Foo&); // assignment operator
// ...
};
标准库通常要求保存在容器中的类型要具有赋值运算符,且其返回值是左侧运算对象的引用。
如果类未定义自己的拷贝赋值运算符,编译器会为类合成一个。一般情况下,合成拷贝赋值运算符(synthesized copy-assignment operator)会将其右侧运算对象的非static
成员逐个赋值给左侧运算对象的对应成员,之后返回左侧运算对象的引用。
// equivalent to the synthesized copy-assignment operator
Sales_data& Sales_data::operator=(const Sales_data &rhs)
{
bookNo = rhs.bookNo; // calls the string::operator=
units_sold = rhs.units_sold; // uses the built-in int assignment
revenue = rhs.revenue; // uses the built-in double assignment
return *this; // return a reference to this object
}
=default
可以通过将拷贝控制成员定义为=default
来显式地要求编译器生成合成版本。
class Sales_data
{
public:
// copy control; use defaults
Sales_data() = default;
Sales_data(const Sales_data&) = default;
~Sales_data() = default;
// other members as before
};
在类内使用=default
修饰成员声明时,合成的函数是隐式内联的。如果不希望合成的是内联函数,应该只对成员的类外定义使用=default
。
只能对具有合成版本的成员函数使用=default
。
阻止拷贝(Preventing Copies)=delete
大多数类应该定义默认构造函数、拷贝构造函数和拷贝赋值运算符,无论是显式地还是隐式地。
在C++11新标准中,将拷贝构造函数和拷贝赋值运算符定义为删除的函数(deleted function)可以阻止类对象的拷贝。删除的函数是一种虽然进行了声明,但是却不能以任何方式使用的函数。定义删除函数的方式是在函数的形参列表后面添加=delete
。
struct NoCopy
{
NoCopy() = default; // use the synthesized default constructor
NoCopy(const NoCopy&) = delete; // no copy
NoCopy &operator=(const NoCopy&) = delete; // no assignment
~NoCopy() = default; // use the synthesized destructor
// other members
};
=delete
和=default
有两点不同:
=delete
可以对任何函数使用;=default
只能对具有合成版本的函数使用,defaulted 函数特性仅用于类的特殊成员函数,且该特殊成员函数没有默认参数。=delete
必须出现在函数第一次声明的地方;=default
既能出现在类内,也能出现在类外。
析构函数不能是删除的函数。对于析构函数被删除的类型,不能定义该类型的变量或者释放指向该类型动态分配对象的指针。
如果一个类中有数据成员不能默认构造、拷贝或销毁,则对应的合成拷贝控制成员将被定义为删除的。
在旧版本的C++标准中,类通过将拷贝构造函数和拷贝赋值运算符声明为private
成员来阻止类对象的拷贝。在新标准中建议使用=delete
而非private
。
继承
#include <iostream>
using namespace std;
// 基类 Shape
class Shape
{
public:
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};
// 基类 PaintCost
class PaintCost
{
public:
int getCost(int area)
{
return area * 70;
}
};
// 派生类
class Rectangle: public Shape, public PaintCost
{
public:
int getArea()
{
return (width * height);
}
};
int main(void)
{
Rectangle Rect;
int area;
Rect.setWidth(5);
Rect.setHeight(7);
area = Rect.getArea();
// 输出对象的面积
cout << "Total area: " << Rect.getArea() << endl;
// 输出总花费
cout << "Total paint cost: $" << Rect.getCost(area) << endl;
return 0;
}
重载 多态 抽象 封装
- 此处不表。注意虚函数的概念
- 虚函数 是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。
- 我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0)
{
width = a;
height = b;
}
// pure virtual function
virtual int area() = 0;
};
- 抽象类:等同于 Java 的接口
#include <iostream>
using namespace std;
// 基类
class Shape
{
public:
// 提供接口框架的纯虚函数
virtual int getArea() = 0;
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};
// 派生类
class Rectangle: public Shape
{
public:
int getArea()
{
return (width * height);
}
};
class Triangle: public Shape
{
public:
int getArea()
{
return (width * height)/2;
}
};
int main(void)
{
Rectangle Rect;
Triangle Tri;
Rect.setWidth(5);
Rect.setHeight(7);
// 输出对象的面积
cout << "Total Rectangle area: " << Rect.getArea() << endl;
Tri.setWidth(5);
Tri.setHeight(7);
// 输出对象的面积
cout << "Total Triangle area: " << Tri.getArea() << endl;
return 0;
}
指针
智能指针
- 知乎:C++智能指针
- 简而言之就是不需要程序员每次去负责指针的释放,而是采用诸如引用计数的方法来自动释放指针。
- C++里面的四个智能指针: auto_ptr, unique_ptr,shared_ptr, weak_ptr 其中后三个是C++11支持,并且第一个已经被C++11弃用。
- 智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。C++ 11中最常用的智能指针类型为shared_ptr,它采用引用计数的方法,记录当前内存资源被多少个智能指针引用。该引用计数的内存在堆上分配。当新增一个时引用计数加1,当过期时引用计数减一。只有引用计数为0时,智能指针才会自动释放引用的内存资源。对shared_ptr进行初始化时不能将一个普通指针直接赋值给智能指针,因为一个是指针,一个是类。可以通过make_shared函数或者通过构造函数传入普通指针。并可以通过get函数获得普通指针。
- 智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针是一个类,当超出了类的实例对象的作用域时,会自动调用对象的析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。
std::unique_ptr
- std::unique_ptr是std::auto_ptr的替代品,其用于不能被多个实例共享的内存管理。这就是说,仅有一个实例拥有内存所有权。它的使用很简单:
class Fraction
{
private:
int m_numerator = 0;
int m_denominator = 1;
public:
Fraction(int numerator = 0, int denominator = 1) :
m_numerator(numerator), m_denominator(denominator)
{
}
friend std::ostream& operator<<(std::ostream& out, const Fraction &f1)
{
out << f1.m_numerator << "/" << f1.m_denominator;
return out;
}
};
int main()
{
std::unique_ptr<Fraction> f1{ new Fraction{ 3, 5 } };
cout << *f1 << endl; // output: 3/5
std::unique_ptr<Fraction> f2; // 初始化为nullptr
// f2 = f1 // 非法,不允许左值赋值
f2 = std::move(f1); // 此时f1转移到f2,f1变为nullptr
// C++14 可以使用 make_unique函数
auto f3 = std::make_unique<Fraction>(2, 7);
cout << *f3 << endl; // output: 2/7
// 处理数组,但是尽量不用这样做,因为你可以用std::array或者std::vector
auto f4 = std::make_unique<Fraction[]>(4);
std::cout << f4[0] << endl; // output: 0/1
cin.ignore(10);
return 0;
}
std::shared_ptr
- std::shared_ptr与std::unique_ptr的主要区别在于前者是使用引用计数的智能指针。引用计数的智能指针可以跟踪引用同一个真实指针对象的智能指针实例的数目。这意味着,可以有多个std::shared_ptr实例可以指向同一块动态分配的内存,当最后一个引用对象离开其作用域时,才会释放这块内存。
#include <iostream>
#include <memory> // for std::shared_ptr
class Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
};
int main()
{
auto ptr1 = std::make_shared<Resource>();
cout << ptr1.use_count() << endl; // output: 1
{
auto ptr2 = ptr1; // 通过复制构造函数使两个对象管理同一块内存
std::shared_ptr<Resource> ptr3; // 初始化为空
ptr3 = ptr1; // 通过赋值,共享内存
cout << ptr1.use_count() << endl; // output: 3
cout << ptr2.use_count() << endl; // output: 3
cout << ptr3.use_count() << endl; // output: 3
}
// 此时ptr2与ptr3对象析构了
cout << ptr1.use_count() << endl; // output: 1
cin.ignore(10);
return 0;
}
std::weak_ptr
- std::shared_ptr可以实现多个对象共享同一块内存,当最后一个对象离开其作用域时,这块内存被释放。但是仍然有可能出现内存无法被释放的情况,联想一下“死锁”现象,对于std::shared_ptr会出现类似的“循环引用”现象:
class Person
{
public:
Person(const string& name):
m_name{name}
{
cout << m_name << " created" << endl;
}
virtual ~Person()
{
cout << m_name << " destoryed" << endl;
}
friend bool partnerUp(std::shared_ptr<Person>& p1, std::shared_ptr<Person>& p2)
{
if (!p1 || !p2)
{
return false;
}
p1->m_partner = p2;
p2->m_partner = p1;
cout << p1->m_name << " is now partenered with " << p2->m_name << endl;
return true;
}
private:
string m_name;
std::shared_ptr<Person> m_partner;
};
int main()
{
{
auto p1 = std::make_shared<Person>("Lucy");
auto p2 = std::make_shared<Person>("Ricky");
partnerUp(p1, p2); // 互相设为伙伴
}
cin.ignore(10);
return 0;
}
- 对象没有被析构!出现内存泄露!仔细想想std::shared_ptr对象是什么时候才能被析构,就是引用计数变为0时,但是当你想析构p1时,p2内部却引用了p1,无法析构;反过来也无法析构。互相引用造成了“死锁”,最终内存泄露!
- std::weak_ptr可以包含由std::shared_ptr所管理的内存的引用。但是它仅仅是旁观者,并不是所有者。那就是std::weak_ptr不拥有这块内存,当然不会计数,也不会阻止std::shared_ptr释放其内存。但是它可以通过lock()方法返回一个std::shared_ptr对象,从而访问这块内存。这样我们可以用std::weak_ptr来解决上面的“循环引用”问题
class Person
{
public:
Person(const string& name):
m_name{name}
{
cout << m_name << " created" << endl;
}
virtual ~Person()
{
cout << m_name << " destoryed" << endl;
}
friend bool partnerUp(std::shared_ptr<Person>& p1, std::shared_ptr<Person>& p2)
{
if (!p1 || !p2)
{
return false;
}
p1->m_partner = p2; // weak_ptr重载的赋值运算符中可以接收shared_ptr对象
p2->m_partner = p1;
cout << p1->m_name << " is now partenered with " << p2->m_name << endl;
return true;
}
private:
string m_name;
std::weak_ptr<Person> m_partner;
};
int main()
{
{
auto p1 = std::make_shared<Person>("Lucy");
auto p2 = std::make_shared<Person>("Ricky");
partnerUp(p1, p2); // 互相设为伙伴
}
cin.ignore(10);
return 0;
}