C++的基本语法
基础
C++部分语法和C还是很像的,例如循环、判断、指针等。不过C++是面向对象的,封装、继承、多态都包含。
有些常用的与C不一样,如输出cout << "x"
,也许初看这个会有难记的感觉,实际上,<<
可以理解成print
, 即cout.print("x")
,如cout << "hello: " << 'paidax'
,即cout.print("hello: ").print("paidax")
, 结尾的endl
也是如此,意思是换行至输出流。
gcc、g++编译C++
gcc也是可以编译C++代码的,只是链接过程不能自动链接C++的库,因此可以有两种方式(设有一个汇编后的test.o
):
1
2
3
4
5
gcc test.cpp -o test2 -lstdC++
// or
gcc test.o -o test -lstdC++
// or
g++ test.o -o test
第一、二个都是一个意思,即使用-l
参数加上C++库,第二个是链接过程用g++命令进行。
变量
C++提供了基本的数据类型,布尔型bool
,字符型char
,整型int
,浮点型float
,双浮点型double
,无类型void
,宽字符型wchar_t
,同样的也能用signed
,unsigned
,short
,long
。其中wchar_t
是 typedef short int wchar_t;
。
数据类型重要的吗,其实不重要,不同的数据类型只是区别分配的内存大小,告诉编译器怎么区解释地址这个值 例如设置type var = 65
,如果type
是char
编译器当成A
, 如果type
是int
,编译器当成65
而已。
numeric_limits
是一个模板类,它提供了有关各种数值类型的特性信息,如最大值、最小值、精度等。
C++也允许定义各种其他类型的变量,比如枚举、指针、数组、引用、数据结构、类等。
type variable_name = value;
声明定义并初始化,同样的,extern
关键字也可使用。
同样的,C++中当局部变量被定义时,系统不会对其初始化,您必须自行对其初始化。定义全局变量时,系统会自动初始化相应值。
类作用域指的是在类内部声明的变量。
除此之外,C++11带来了auto
类型
1
2
3
auto a = 10;
a = "str";
auto b = "str";
auto
会自动推断字面量是什么类型,所以这里必须初始化,其次,上述代码第二行会报错,因为第一行会把a
自动定为 int
类型,无法将const char[]
赋给int
,注意gcc编译时需加上-std=c++11
参数。
函数
同C:
1
2
3
4
5
6
return_type function_name( parameter list )
{
body of the function
}
// 函数声明
return_type function_name( parameter list );
当您定义一个函数,您可以为参数列表中后边的每一个参数指定默认值。当调用函数时,如果实际参数的值留空,则使用这个默认值,如int max(int a, int b = 10);
C++11提供了对匿名函数的支持,称为Lambda函数(也叫Lambda表达式)。
头文件
头文件其实要和include
来说,设main
中需要调用的一个函数,那需要在main
之前声明这个函数, 告知编译器有这个函数,include
头文件也一样,include
在预处理的时候将内容复制一份到你的文件中, 那么在main
中使用到的include
文件中的内容不就被声明了吗?就是这样。
一般来说<>
包含编译器路径,""
包含很多,例如自己写的用""
包裹, 这是因为自己写的一般在当前文件夹下,""
会搜索当前路径例如"/your_path/xxx"
, "iostream"
也是可以的,""
可以包含路径。
#pragma once
可以告诉编译器预处理时请只复制一份这个头文件,毕竟如果有变量, 那预处理又是复制粘贴,有些变量就会被定义多次实现只复制一次也不仅这个方法,还有预处理#ifndef
等。
C++标准库一般没h
结尾,C有,不要疑惑,这只是C++的开发者想区分一下两者的标准库罢了。
指针
同C一样,C++指针也通过*
,&
进行取值、取地址操作。
一样的,void* a = NULL;
定义了一个NULL
指针。
NULL
指针还可以用来做if
判断,因为0
是假,计算机不知道什么是假内存中各bit为0
是假, 而NULL
指针正好是0
,所以如果想知道这个指针是不是NULL
指针,if
它就好了。
事实上,vs这些IDE或者命令行可以反汇编二进制文件,查看if
那语句的反汇编代码会发现, 如果条件不成立,会置0
到地址中去。
引用
引用变量是一个别名,也就是说,它是某个已存在变量的另一个名字。一旦把引用初始化为某个变量,就可以使用该引用名称或变量名称来指向变量。
与指针不同的是:
- 不存在空引用。引用必须连接到一块合法的内存。
- 一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象。
- 引用必须在创建时被初始化。指针可以在任何时间被初始化。
创建引用后可以通过原始变量或引用来操作变量,引用符号为&
。通过引用,我们可以方便地对变量进行别名操作,提高代码的可读性和简洁性。引用不存在空引用,在初始化后不能改变指向,提供了更高的安全性。当然,编码安全更好。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 声明简单的变量
int i;
double d;
// 声明引用变量
int& r = i;
double& s = d;
i = 5;
cout << "Value of i : " << i << endl;
cout << "Value of i reference : " << r << endl;
d = 11.7;
cout << "Value of d : " << d << endl;
cout << "Value of d reference : " << s << endl;
// output:
// Value of i : 5
// Value of i reference : 5
// Value of d : 11.7
// Value of d reference : 11.7
引用可以用作参数或返回值。
引用只是指针的拓展,真正要理解的是指针,或者说它是指针的语法糖,引用即字面意思, 它是一个引用,没有实际的存储,就像它只需要type& ref = var
,var
前面不需要用什么&
这样的字符,再如下面代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void addself(int a) {
a++;
}
// 实际上调用这个函数传入参数,a不会自增,这也是很好理解的,int a实际上是复制了一份
// 真正复制了一份,和传入的叫什么没关系,如果要实现自增,那就传地址,也可以传一个引用
void addself(int* a) {
(*a)++;
}
// 形参是地址,显然调用需要传入地址,如addself(&var)
// 注意这里*a要加括号,不然就是对地址自增了
void addself(int& a) {
a++;
}
// 调用这个也可以实现自增,这就是引用,如addself(var)
// 这里var前面不需要加什么&这种奇怪的字符,引用不创建存储地址,只是一个引用罢了
// 语法相比较指针简洁了很多,这就是引用
指针和引用作为返回值
C++自由度很高,语法上将*/&
放在函数前即告诉编译器该函数返回值是什么。
指针作为返回值,例1:
1
2
3
4
5
6
7
8
9
10
11
12
13
int* return1(int arr[]) {
return arr+1;
}
int main() {
using namespace std;
int arr[] = {10, 100, 1000};
*return1(arr) = 1111;
cout << arr[1] << endl;
int *b = return1(arr);
cout << b << endl;
cout << return1(arr) << endl;
cout << arr+1 << endl;
}
其中return1
返回一个地址,所以函数体最后返回需要返回一个地址,其次,return1(arr)
在接收时是接了个地址, 所以前面需要用*
进行取值赋值操作,此时,如果打印结尾三行的结果,可以发现地址是一样的。
引用作为返回值,例2:
1
2
3
4
5
6
7
8
9
10
11
12
13
int& return1(int arr[]){
return arr[1];
}
int main() {
using namespace std;
int arr[] = {10, 100, 1000};
return1(arr) = 2222;
cout << arr[1] << endl;
int &b = return1(arr);
cout << &b << endl;
cout << &(return1(arr)) << endl;
cout << arr+1 << endl;
}
同指针,这里最后三行的结果打印出的地址也是一致的,注意,返回值是引用,所以这里需要int &b = return1(arr);
接收参数,而cout << &b << endl;
是取地址,这两个&
不一样。
面向对象
类
类是C++的核心特性,通常被称为用户定义的类型。
类用于指定对象的形式,是一种用户自定义的数据类型,它是一种封装了数据和函数的组合。类中的数据称为成员变量,函数称为成员函数。类可以被看作是一种模板,可以用来创建具有相同属性和行为的多个对象。
1
2
3
4
5
class class_name{
Access specificers://访问修饰符 private, protected, public
var;
func;
};
如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Box
{
public:
double length; // 盒子的长度
double breadth; // 盒子的宽度
double height; // 盒子的高度
double get(void); // 可以在类中声明函数
void set( double len, double bre, double hei );
};
//在外部定义成员函数
double Box::get(void)
{
return length * breadth * height;
}
// 实例
Box Box1; // 声明 Box1,类型为 Box
Box Box2; // 声明 Box2,类型为 Box
类的对象的公共数据成员可以使用直接成员访问运算符.
来访问。
1
2
Box1.height = 5.0;
Box1.get();
类的数据成员和函数成员都可以被声明为静态static
的。 这里再提一下static
,这意味着只能在此文件被访问,要知道,全局变量是这个程序中各个文件都可以用的,static
将变量的空间分配到静态区,只有当源程序结束时才会被释放,而操作系统将局部变量分配在栈区,函数调用结束后就会回收。
类的构造函数
类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。
构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回void
。构造函数可用于为某些成员变量设置初始值。
如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Line
{
public:
void setLength( double len );
double getLength( void );
Line(); // 这是构造函数
private:
double length;
};
// 成员函数定义,包括构造函数
Line::Line(void)
{
cout << "Object is being created" << endl;
}
构造函数也可带参数。
析构函数
同样的,类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。
析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。
1
2
3
4
5
6
7
...
~Line();
...
Line::~Line(void)
{
cout << "Object is being deleted" << endl;
}
继承
继承代表了is a
关系。例如,哺乳动物是动物,狗是哺乳动物,因此,狗是动物,等等。
语法:
1
class derived-class: access-specifier base-class
其中,访问修饰符access-specifier
是public
、protected
或private
其中的一个,base-class
是之前定义过的某个类的名称。如果未使用访问修饰符access-specifier
,则默认为private
,如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 基类
class Shape
{
public:
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);
}
};
派生类可以访问基类中所有的非私有成员(public
,protected
)。因此基类成员如果不想被派生类的成员函数访问,则应在基类中声明为private
。
一个派生类继承了所有的基类方法,但下列情况除外:
- 基类的构造函数、析构函数和拷贝构造函数。
- 基类的重载运算符。
- 基类的友元函数。
多继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,…
{
<派生类类体>
};
// 如上述加上基类 PaintCost
class PaintCost
{
public:
int getCost(int area)
{
return area * 70;
}
};
// 派生类
class Rectangle: public Shape, public PaintCost
{
public:
int getArea()
{
return (width * height);
}
};
重载运算符和重载函数
C++允许在同一作用域中的某个函数和运算符指定多个定义,分别称为函数重载和运算符重载。
当您调用一个重载函数或重载运算符时,编译器通过把您所使用的参数类型与定义中的参数类型进行比较,决定选用最合适的定义。
函数重载很好理解,即在同一个作用域内,可以声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同。这里不能仅通过返回类型的不同来重载函数,如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class printData
{
public:
void print(int i) {
cout << "整数为: " << i << endl;
}
void print(double f) {
cout << "浮点数为: " << f << endl;
}
void print(char c[]) {
cout << "字符串为: " << c << endl;
}
};
想想C中的printf
,需要指定类型,这里不需要。
运算符重载就是字面意思可以重新设置运算符的功能,如将+
拓展为可以相加两个对象,如:
1
Box operator+(const Box&);
C++大部分运算符是可以重载的,如算术、逻辑、关系运算符等,但成员访问.
,预处理#
是不能重载的。
多态
面向对象三大特性中多态是比较难理解的,其实就是字面意思同样的方法不同类型的对象调用有不同的效果,当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。
C++多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数,注意:
- 被调用的函数必须是虚函数(virtual),并且派生类必须对基类的虚函数进行重写。
- 必须通过基类的指针或者引用调用虚函数
如不同年龄对买票行为票价的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Person
{
public:
// 虚函数
virtual void BuyTicket()
{
cout << "全价买票" << endl;
}
protected:
};
class Student :public Person
{
public:
// 虚函数重写
virtual void BuyTicket()
{
cout << "半价买票" << endl;
}
// 派生类的虚函数不加virtual关键字,也可以构成重写,但是这种写法不规范,不建议这样写。
// void BuyTicket()
// {
// cout << "半价买票" << endl;
// }
protected:
};
C++接口(抽象类)
接口描述了类的行为和功能,而不需要完成类的特定实现。C++类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。纯虚函数是通过在声明中使用= 0
来指定的,如:
1
2
3
4
5
6
7
8
9
10
class Box
{
public:
// 纯虚函数
virtual double getVolume() = 0;
private:
double length; // 长度
double breadth; // 宽度
double height; // 高度
};
面向对象的系统可能会使用一个抽象基类为所有的外部应用程序提供一个适当的、通用的、标准化的接口。然后,派生类通过继承抽象基类,就把所有类似的操作都继承下来。
C++相关概念
在基础和面向对象中,有一些语法、设计到的概念未解释,如namespace
,虚函数等。
命名空间
命名空间其实就是定义了一个范围,很实际的场景,无法保证不同文件中不出现相同名称的函数。
命名空间的定义和调用:
1
2
3
4
namespace namespace_name {
// 代码声明
}
namespace_name::code; // code 可以是变量或函数
using namespace
指令,就可以不用在前面加上命名空间的名称,指令会告诉编译器,代码使用指定的命名空间中的名称。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
using namespace std;
// 第一个命名空间
namespace first_space{
void func(){
cout << "Inside first_space" << endl;
}
}
// 第二个命名空间
namespace second_space{
void func(){
cout << "Inside second_space" << endl;
}
}
using namespace first_space;
int main ()
{
// 调用第一个命名空间中的函数
func();
return 0;
}
using
指令也可以用来指定命名空间中的特定项目,如using std::cout;
。
查看<iostream>
,也会发现其中内容写在了命名空间中:
1
2
3
4
namespace std _GLIBCXX_VISIBILITY(default)
{
...
}
动态内存
C++程序中的内存分为两个部分:
- 栈:在函数内部声明的所有变量都将占用栈内存。
- 堆:这是程序中未使用的内存,在程序运行时可用于动态分配内存。
使用new
运算符来为任意的数据类型动态分配内存:new data-type;
,如:
1
2
double* pvalue = NULL; // 初始化为 null 的指针
pvalue = new double; // 为变量请求内存
new
操作不一定能正常分配内存,检查new
运算符是否返回NULL
指针,并采取以下适当的操作:
1
2
3
4
5
6
7
double* pvalue = NULL;
if( !(pvalue = new double ))
{
cout << "Error: out of memory." <<endl;
exit(1);
}
new
相比较malloc()
不只是分配了内存,它还创建了对象。删除分配的内存:
1
delete pvalue; // 释放 pvalue 所指向的内存