变量声明和定义的关系
C++支持分离式编译机制,该机制允许将程序分割为若干个文件,每个文件可被独立编译。这就需要在文件间共享代码的方法。例如:一个文件的代码可能需要使用另一个文件中定义的变量。
声明:使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明;
定义:负责创建与名字关联的实体。
变量声明规定了变量的类型和名字,这一点和定义相同。但除此之外,定义还申请了存储空间。
如果想声明一个变量而非定义他,就在变量名前添加extern关键字
extern int i; // 声明i而非定义i
int j; // 声明并定义了j
变量只能被定义一次,但是可以被多次声明。
作用域操作符
#include <iostream>
int reused = 42; // reused 具有全局作用域
int main()
{
int reused = 0; // 新建局部变量reused,覆盖了全局变量reused
// 输出局部变量reused
std::cout << reused << endl;
// 显示访问全局变量reused
std::cout << ::reused << endl;
}
引用
- 对象的另一个名字
- 引用必须被初始化
- 为引用赋值,实际上是把值赋给了与引用绑定的对象。获取引用的值,实际上是获取了与引用绑定的对象的值。同理,以引用作为初始值,实际上是以与引用绑定的对象作为初始值。
- 引用本身不是一个对象,所以不能定义引用的引用
- 引用只能绑定在对象上,不能与某个字面值或表达式的计算结果绑定在一起
int &refVal = 10; // 错误:引用类型的初始值必须是一个对象
double dval = 3.14;
int &refVal5 = dval; // 错误:引用类型要和与之绑定的对象类型匹配
指针
和引用类似:指针也可以实现对其它对象间接访问
问:指针可以指向引用吗?
答:因为引用不是对象,没有实际地址,不能定义指向引用的指针。
指针类型和它所指向的对象类型要严格匹配:
double dval;
int *pi = &dval; // 错误:试图把double型对象的地址赋给int型指针
1. 空指针
// nullptr C++11的字面值
int *p1 = nullptr;
int *p2 = 0;
// 预处理变量NULL在头文件cstdlib中定义,它的值就是0
int *p3 = NULL;
2. 指针相等
- 都为空
- 都指向同一个对象
- 都指向同一个对象下一地址
3. void* 指针
特殊的指针类型,用于存放任意对象的地址。一个void* 指针存放一个地址,这个和其它指针类型一样,不同的是:void* 指向的地址中是一个什么类型的对象不了解。
4. 复合类型的两种写法
int *p1, *p2; // p1, p2 都是指向int的指针
int *p1, p2; // p1是指向int的指针,p2是int
这种写法把修饰符和变量标识符写在一起,着重强调变量具有的复合类型。
int* p1;
int* p2;
这种形式着重强调本次声明定义了一种复合类型。
这两种写法没有孰对孰错之分,要统一就行。
5. 指向指针的指针
int iVal = 1024;
int *pi = &iVal; // pi指向一个int型
int **ppi = π // ppi指向一个int型的指针
6. 指向指针的引用
引用本身不是一个对象,因此指针不能指向引用。但是,指针是对象,所以存在对指针的引用。
int i = 42;
int *p; // p是一个int型指针
int *&r = p; // r是一个对指针p的引用
r = &i; // r引用了一个指针,因此给r赋值&i,就是令p指向i
*r = 0; // 解引用r得到i,也就是p指向的对象,将i的值改为0
注意:要理解r的类型到底是什么,最简单的方法就是从右向左阅读r的定义。离变量名最近的符号(此例中&r的符号&)对变量的类型有最直接的影响,因此r是一个引用。声明符的其余部分用以确定r引用的类型是什么,此例中的符号*说明r引用的是一个指针。最后,声明的基本数据类型部分指出r引用的是一个int指针。
const限定符
1. 常量定义
用于定义常量。const对象必须初始化
const int j = 42; // 编译时初始化,编译器会把所有的j替换成42
const int j = get_size(); // 运行时初始化
2. 多个文件共享const变量
默认状态下,const对象仅在文件内有效, 当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。
如果想要const变量在文件之间共享,可以这么做:
// file_1.cpp 定义并初始化常量,加了extern表示该常量能被其他文件访问
extern const int bufSize = fcn();
// file_1.h 头文件。声明bufSize,加上extern表名bufSize是在其它文件中定义的
extern const int bufSize;
注意:如果想在多个文件中共享const对象,必须在变量的定义之前添加extern关键字
3. 引用和const
与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象
const int ci = 1024;
const int &r1 = ci; // 正确:引用及其对应的对象都是常量
r1 = 42; // 错误:r1是对常量的引用,不能改变引用对象的值
int &r2 = ci; // 错误:试图让一个非常量引用指向一个常量对象
4. 引用类型必须与其所引用对象的类型一致
但有两个例外,其中一个就是const
double dval = 3.14;
const int &ri = dval; // 正确:const int &允许绑定到普通的double对象上
int &ri = dval; // 错误:类型不一致,编译报错
可以这么做的原因,编译器背后做了如下工作:
const int temp = dval; // 由double生成一个临时的整型常量
const int &ri = temp; // 让ri绑定这个临时量
接下来探讨下如果ri不是常量,会有什么样的后果。如果ri不是常量,就允许对ri赋值,这样就会改变ri所引用对象的值。注意,此时绑定的对象是一个临时量而非dval。程序员既然让ri引用dval,就肯定想通过ri改变dval的值,否则干什么要给ri赋值呢?所以C++会把这种行为归为非法。
5. 常量引用可以引用一个非const对象
int i = 42;
int &r1 = i; // 引用r1绑定对象i
const int &r2 = i; // 正确: 引用R2也绑定对象i,但不允许通过r2修改i的值
r1 = 0; // 正确:r1非常量,i的值修改为0
r2 = 0; // 错误:r2是一个常量引用
6. 指针和const
指向常量的指针不能用于改变其所指向对象的值。要想存放常量对象的地址,只能使用指向常量的指针。
const double pi = 3.14; // pi常量
double *ptr = π // 错误:ptr是一个普通指针,不能指向常量
const double *cptr = &pt; // 正确:cptr是一个指向常量的指针,可以指向pi
*cptr = 42; // 错误:不能给*cptr赋值
和引用类似,指针类型必须与其所指向对象的类型一致。但也有两个例外,第一个例外就是:允许令一个指向常量的指针指向一个非常量对象。
double dval = 3.14; // dval是一个double类型变量
const double *cptr = &dval; // 正确:但不能通过cptr改变dval的值
和引用类似,指向常量的指针也没有规定其所指向的对象必须是常量。
7. 常量指针
常量指针:指针本身是一个常量。必须初始化,一旦初始化完成,它的值不能再改变。
语法:把*放在const关键字之前说明指针是一个常量指针。这样书写隐含一层意味:不变的是指针本身的值而非指向的那个值
int errNumb = 0;
int *const curErr = &errNumb; // curErr是常量指针,将一直指向errNumb
const double pi = 3.14;
const double *const pip = π // pip是一个指向常量对象的常量指针
从右往左阅读法则,是理解这些声明最行之有效的方法。比如:离curErr最近的符号是const,意味着curErr本身是一个常量对象。声明符的下一个符号是*,意味着curErr是一个常量指针。最后,该声明语句的基本数据类型部分确定了常量指针指向的是一个int对象。与之相似,pip是一个常量指针,它指向的对象是一个double型常量。
string类型
1. 命名空间using声明
using namespace::name;
#include <string>
#include <iostream>
using std::cin; using std::cout; using std::endl;
using std::string;
2. string::size_type类型
string的size函数,返回的类型是 string::size_type,注意:它是一个无符号类型。所以表达式中不能混用带符号的数,否则产生意想不到的结果。
auto len = line.size(); // len的类型是string::size_type
int n = -10;
// 这个表达式可能恒为true,因为n会被转成无符号数,变成一个很大的整数。
line.size() < n;
如果表达式中已经有了size()函数就不要再使用int了,这样可以避免混用int和unsigned可能带来的问题。
3. 为string对象赋值
string str1(10, 'c'), str2; // str1的内容是cccccccccc;str2是一个空字符串
str1 = str2; // 赋值:str2的副本替换str1的内容,此时str1和str2都是空字符串
3. 两个string相加
string s1 = "hello, ", s2 = "world\n";
string s3 = s1 + s2; // s3的内容:hello, world\n
s1 += s2; // 等价于s1 = s1 + s2
4. 字面值和string对象相加
注意:当把string对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符(+)的两侧的运算对象至少有一个是string
string s4 = s1 + ", "; // 正确:把一个string对象和一个字面值相加
string s5 = "hello" + ", "; // 错误:两个运算符对象都不是string
string s6 = s1 + ", " + "world"; // 正确:s1 + ", "结果是一个string对象
string s7 = "hello" + ", " + s2; // 错误:"hello" + ", " 两侧都不是string
5. 范围for循环
统计给定字符串中,标点符号的个数:
int main(int argc, char const *argv[])
{
string s("hello world!!!");
int cnt = 0;
for (char c : s) {
if (ispunct(c)) {
cnt++;
}
}
cout << cnt << " punctuation characters in " << s << endl;
}
使用范围for循环改变字符串中的字符:引用。
使用引用作为循环控制变量,这个变量实际上被依次绑定到了序列的每个元素上。
string s("Hello World!!!");
// 转换成大写形式
for (auto &c : s) { // s中的每个字符(注意:c是引用)
c = toupper(c); // c是一个引用,因此赋值语句将改变s中字符的值
}
count << s << endl;
6. 下标运算符 [ ]
下标运算符接收的输入参数是string::size_type类型的值。
string对象的下标必须大于等于0而小于s.size()。使用超出此范围的下标将引发不可预知的结果,以此推断,使用下标访问空string也会引发不可预知的结果。
下面代码是一种安全访问:
if (!s.empty()) {
cout << s[0] << endl;
}
不管什么时候,只要对string使用下标,都要确认在那个位置上确实有值。
例子:把一个字符串中,第一个单次改成大写
string s("some string");
for (decltype(s.size()) index = 0; index != s.size() && !isspace(s[index]); ++index) {
s[index] = toupper(s[index]);
}
// 结果: SOME string
向量和迭代器
1. 把string对象中第一个单词改写成大写形式
string s = "hello world";
for (auto it = s.begin(); it != s.end() && !isspace(*it); ++it) {
*it = toupper(*it);
}
2. 注意
- 不能在范围for循环中想vector对象中添加元素
- 但凡使用了迭代器的循环体,都不要向迭代器所属的容器中添加元素
数组
1. begin 和 end
C++11中引入了两个名为begin和end的函数
int ia[] = {0,1,2,3,4,5,6,7,8,9};
int *beg = begin(ia); // 指向ia首元素的指针
int *last = end(ia); // 指向arr尾元素的下一位置的指针
示例:找出arr中第一个负数:
// pbeg指向arr的首元素,pend指向arr尾元素的下一位置
int *pbeg = begin(arr), *pend = end(arr);
while(pbeg != bend && *pbeg >= 0) {
++pbeg;
}
2. 使用数组初始化vector
int int_arr[] = {0, 1, 2, 3, 4, 5};
// ivec有6个元素,分别是int_arr中对应元素的副本
vector<int> ivec(begin(int_arr), end(int_arr));
构造函数
1. 拷贝构造函数
class Person {
public:
Person() {
this->name = NULL;
this->work = NULL;
}
Person(char *name, int age, char *work = "none") {
this->age = age;
this->name = new char[strlen(name) + 1];
strcpy(this->name, name);
this->work = new char[strlen(work) + 1];
strcpy(this->work, work);
}
~Person() {
cout << "~Person() ";
if (this->name) {
cout << "name="<<this->name<<endl;
delete this->name;
}
if (this->work) {
delete this->work;
}
}
void print_info() {
cout<<"name="<<this->name<<", age="<<age<<", work="<<work<<endl;
}
private:
char *name;
char *work;
int age;
};
void test_func() {
Person per("zhangsan", 32);
// 调用默认的拷贝构造函数,进行值拷贝
Person per2(per);
per2.print_info();
}
test_func()中 Person per2(per);会调用默认的拷贝构造函数构造per2,进行值拷贝。相当于:
Person(Person &person) {
cout << "Person(Person &person)" << endl;
this->age = person.age;
this->name = person.name;
this->work = person.work;
}
结果是per中的name和per2中的name指向同一块内存,work也是同理。这样导致per和per2销毁时,name和work会被delete两次,第二次delete会崩溃。
为了解决这个问题,需要重写拷贝构造函数:
Person(Person &person) {
cout << "Person(Person &person)" << endl;
this->age = person.age;
this->name = new char[strlen(person.name) + 1];
strcpy(this->name, person.name);
this->work = new char[strlen(person.work) + 1];
strcpy(this->work, person.work);
}
2. 构造函数执行顺序
阅读如下代码:
class Person {
public:
Person() {
this->name = NULL;
this->work = NULL;
}
Person(char *name, int age, char *work = "none") {
cout << "Person(...) " << "name=" << name << endl;
this->age = age;
this->name = new char[strlen(name) + 1];
strcpy(this->name, name);
this->work = new char[strlen(work) + 1];
strcpy(this->work, work);
}
~Person() {
cout << "~Person() ";
if (this->name) {
cout << "name="<<this->name<<endl;
delete this->name;
}
if (this->work) {
delete this->work;
}
}
private:
char *name;
char *work;
int age;
};
Person per_g("per_g", 10);
void test_func() {
Person per_func("test_func", 32);
static Person per_func_s("test_func_s", 32);
}
int main(int argc, char *argv[]) {
Person per_main("per_main", 10);
static Person per_main_s("per_main_s", 10);
for (int i = 0; i < 2; i++) {
test_func();
Person per_for("per_for", 10);
}
return 0;
}
输出结果:
Person(...) name=per_g
Person(...) name=per_main
Person(...) name=per_main_s
Person(...) name=test_func
Person(...) name=test_func_s
~Person() name=test_func
Person(...) name=per_for
~Person() name=per_for
Person(...) name=test_func
~Person() name=test_func
Person(...) name=per_for
~Person() name=per_for
~Person() name=per_main
~Person() name=test_func_s
~Person() name=per_main_s
~Person() name=per_g
总结:
- 全局对象最先构造:per_g
- static对象只会构造一次
- for循环里面的对象,每次遍历都会构造,析构
- 程序执行结束会自动析构static对象和全局对象
3. 成员对象构造
class Person {
public:
Person() {
cout << "Person()" << endl;
this->name = NULL;
this->work = NULL;
}
Person(char *name, int age, char *work = "none") {
this->age = age;
this->name = new char[strlen(name) + 1];
strcpy(this->name, name);
this->work = new char[strlen(work) + 1];
strcpy(this->work, work);
}
~Person() {
cout << "~Person()" << endl;
if (this->name) {
cout << "name="<<this->name<<endl;
delete this->name;
}
if (this->work) {
delete this->work;
}
}
void print_info() {
cout<<"name="<<this->name<<", age="<<age<<", work="<<work<<endl;
}
private:
char *name;
char *work;
int age;
};
class Student {
private:
Person father;
Person mother;
int student_id;
public:
Student() {
cout << "Student()" << endl;
}
};
int main(int argc, char *argv[]) {
// 构造顺序?
Student s;
return 0;
}
Student类有两个Person成员变量,分别是father和mother,构造顺序如下:
Person()
Person()
Student()
~Person()
~Person()
结论:先构造成员变量,father和mother,再构造自己student。
4. 构造自己同时构造成员变量
class Person {
public:
Person() {
cout << "Person()" << endl;
this->name = NULL;
this->work = NULL;
}
Person(char *name, int age, char *work = "none") {
cout << "(char *name, int age, char *work = \"none\")" << " name=" << name << endl;
this->age = age;
this->name = new char[strlen(name) + 1];
strcpy(this->name, name);
this->work = new char[strlen(work) + 1];
strcpy(this->work, work);
}
~Person() {
cout << "~Person()" << endl;
if (this->name) {
cout << "name="<<this->name<<endl;
delete this->name;
}
if (this->work) {
delete this->work;
}
}
void print_info() {
cout<<"name="<<this->name<<", age="<<age<<", work="<<work<<endl;
}
private:
char *name;
char *work;
int age;
};
class Student {
private:
Person father;
Person mother;
int student_id;
public:
Student() {
cout << "Student()" << endl;
}
// 构造自己同时,构造成员变量
Student(int id, char *father_name, char * mother_name, int father_age, int mother_age) :
father(father_name, father_age),
mother(mother_name, mother_age) {
cout << "Student(int id, char *father_name, char * mother_name, int father_age, int mother_age)" << endl;
}
~Student() {
cout << "~Student()" << endl;
}
};
int main(int argc, char *argv[]) {
Student s(111, "Bill", "Lily", 32, 30);
return 0;
}
执行结果:
(char *name, int age, char *work = "none") name=Bill
(char *name, int age, char *work = "none") name=Lily
Student(int id, char *father_name, char * mother_name, int father_age, int mother_age)
~Student()
~Person()
name=Lily
~Person()
name=Bill
静态成员变量
需求:计算一个类有多少个对象?
可通过static成员来统计,代码如下:
class Person {
private:
char *name;
char *work;
int age;
// 统计一个类有多少个对象
static int cnt;
public:
static int get_count() {
return cnt;
}
Person() {
this->name = NULL;
this->work = NULL;
cnt++;
}
Person(char *name, int age, char *work = "none") {
this->age = age;
this->name = new char[strlen(name) + 1];
strcpy(this->name, name);
this->work = new char[strlen(work) + 1];
strcpy(this->work, work);
cnt++;
}
~Person() {
if (this->name) {
cout << "name="<<this->name<<endl;
delete this->name;
}
if (this->work) {
delete this->work;
}
cnt--;
}
void print_info() {
cout<<"name="<<this->name<<", age="<<age<<", work="<<work<<endl;
}
};
// 这句很关键:Person::cnt定义和初始化
// 如果没有这句,Person::get_count()会报错,提示找不到Person::cnt
int Person::cnt = 0;
int main(int argc, char *argv[]) {
Person per1;
Person per2;
Person per3;
Person per4;
Person per5;
Person *per6 = new Person[10];
cout << "person number = " << Person::get_count() << endl;
return 0;
}
输出:
person number = 15
友元
下面程序,实现两个Point相加
class Point {
private:
int x;
int y;
public:
Point() {}
Point(int x, int y) : x(x), y(y) {
}
int get_x() { return x; }
int get_y() { return y; }
void set_x(int x) { this->x = x; }
void set_y(int y) {this->y = y; }
void print_info() {
cout << "(" << x << ", " << y << ")" << endl;
}
};
// 实现两个point相加
Point add(Point &p1, Point &p2) {
Point ret = Point();
ret.set_x(p1.get_x() + p2.get_x());
ret.set_y(p1.get_y() + p2.get_y());
return ret;
}
int main(int argc, char *argv[]) {
Point p1(1, 2);
Point p2(2, 3);
Point p3 = add(p1, p2);
p3.print_info();
return 0;
}
看到add函数实现两个point相加,调用了Point里面很多函数,相对繁琐。
如果将add函数声明成Point类的友元,add函数就可以直接访问Point的私有成员;代码如下:
class Point {
private:
int x;
int y;
public:
Point() {}
Point(int x, int y) : x(x), y(y) {
}
int get_x() { return x; }
int get_y() { return y; }
void set_x(int x) { this->x = x; }
void set_y(int y) {this->y = y; }
void print_info() {
cout << "(" << x << ", " << y << ")" << endl;
}
friend Point add(Point &p1, Point &p2);
};
// add函数是Point的友元,可以直接访问Point的私有成员。简化代码调用。
Point add(Point &p1, Point &p2) {
Point ret = Point();
ret.x = p1.x + p2.x;
ret.y = p1.y + p2.y;
return ret;
}
int main(int argc, char *argv[]) {
Point p1(1, 2);
Point p2(2, 3);
Point p3 = add(p1, p2);
p3.print_info();
return 0;
}