char类型
C++对字符使用单引号,对字符串使用双引号。字符用单引号括起,表示字符的数值编码。
int main(void) {
using namespace std;
char ch = 'M'; // 将 M 的 ASCII 码分配给 ch
int i = ch; // 将相同的代码存储在 int 中
cout << "The ASCII code for " << ch << " is " << i << endl;
cout << "Add one to the character code:" << endl;
ch = ch + 1; // 更改 ch 中的字符代码
i = ch; // 在 i 中保存新的字符代码
//输出时,cout将值78转换为所显示的字符N;
cout << "The ASCII code for " << ch << " is " << i << endl;
// using the cout.put() member function to display a char
cout << "Displaying char ch using cout.put(ch): ";
cout.put(ch);
// using cout.put() to display a char constant
cout.put('!');
cout << endl << "Done" << endl;
return 0;
}
/*输出结果
The ASCII code for M is 77
Add one to the character code:
The ASCII code for N is 78
Displaying char ch using cout.put(ch): N!
Done
*/
数组
声明数组的通用格式:typeName arrayName[arraySize]
注意:数组最后一个元素的索引比数组长度小1.如下图
初始化方式:
int cards[4] = {3,4,5,6,78};
int cards[4] {3,4,5,6,78}; //c++ 11
int cards[4];
上面几种方式都可以。
C-风格字符串
C-风格字符串具有一种特殊的性质: 以空字符 (null character) 结尾,空字符被写作\0, 其 ASCII 码为0, 用来标记字符串的结尾。
char dog[8]={ 'b', 'e', '矿, 'U I I '文, I', 'I', 'I'};// not a string
char cat[8]={'f'' 'a', 't'' 'e'' 's'' 's', 'a','\0'} ;//a string
字符串常量(字符串字面值)
char bird[ll] = "Mr. Cheeps";
char fish[] = "Bubbles";
在确定存储宇符串所需的最短数组时, 别忘了将结尾的空宇符计算在内。
数组中使用字符串
int main(void) {
using namespace std;
const int Size = 15;
char name1[Size]; // 空数组
char name2[Size] = "C++owboy"; // 初始化数组
cout << "Howdy! I'm " << name2;
cout << "! What's your name?\n";
cin >> name1;
cout << "Well, " << name1 << ", your name has ";
//strlen返回的是存储在数组中的字符串的长度, 而不是数组本身的长度。
//另外, strlen()只计算可见的字符, 而不把空字符计算在内.
cout << strlen(name1) << " letters and is stored\n";
//计算大小
cout << "in an array of " << sizeof(name1) << " bytes.\n";
cout << "Your initial is " << name1[0] << ".\n";
name2[3] = '\0'; // set to null character
cout << "Here are the first 3 characters of my name: ";
cout << name2 << endl;
return 0;
}
/*打印
Howdy! I'm C++owboy! What's your name?
kim
Well, kim, your name has 3 letters and is stored
in an array of 15 bytes.
Your initial is k.
Here are the first 3 characters of my name: C++
*/
上面程序使用‘\0’截断了数组,具体原理如下图:
使用new创建动态数组
int * psome = new int[10];
//创建一个包含10个int元素的数组
//new运算符返回第一个元素的地址
//注意:使用new创建的数组,需要delete[]psome
怎么访问数组的元素?
只要把指针当作数组名使用(c++将数组名解释为地址)即可。对于第一个元素,可以使用psome[0],第二个元素使用psome[1],依此类推。
指针和数组基本等价的原因在于指针算术和C++内部处理数组的方式。
例如:
将整数变量加1后,其值将增加1;但将指针变量加1后, 增加的量等千它指向的类型
的字节数。 将指向double的指针加1后,如果系统对double使用 8个字节存储, 则数值将增加 8;将指向
short的指针加1后, 如果系统对sho rt 使用2 个字节存储, 则指针值将增加2。
int main(void) {
using namespace std;
double wages[3] = {10000.0, 20000.0, 30000.0};
short stacks[3] = {3, 2, 1};
// 这里有两种获取数组地址的方法
double *pw = wages; // 数组名 = 地址
short *ps = &stacks[0]; // 或使用地址运算符
//带数组元素
cout << "pw = " << pw << ", *pw = " << *pw << endl;
pw = pw + 1;
cout << "add 1 to the pw pointer:\n";
cout << "pw = " << pw << ", *pw = " << *pw << "\n\n";
cout << "ps = " << ps << ", *ps = " << *ps << endl;
ps = ps + 1;
cout << "add 1 to the ps pointer:\n";
cout << "ps = " << ps << ", *ps = " << *ps << "\n\n";
cout << "access two elements with array notation\n";
cout << "stacks[0] = " << stacks[0]
<< ", stacks[1] = " << stacks[1] << endl;
cout << "access two elements with pointer notation\n";
cout << "*stacks = " << *stacks
<< ", *(stacks + 1) = " << *(stacks + 1) << endl;
cout << sizeof(wages) << " = size of wages array\n";
cout << sizeof(pw) << " = size of pw pointer\n";
return 0;
}
/*output
pw = 0x71601ff880, *pw = 10000
add 1 to the pw pointer:
pw = 0x71601ff888, *pw = 20000
ps = 0x71601ff87a, *ps = 3
add 1 to the ps pointer:
ps = 0x71601ff87c, *ps = 2
access two elements with array notation
stacks[0] = 3, stacks[1] = 2
access two elements with pointer notation
*stacks = 3, *(stacks + 1) = 2
24 = size of wages array
8 = size of pw pointer
*/
指针和字符串
直接看个例子:
int main(void) {
using namespace std;
char animal[20] = "bear";
//将char指针初始化为指向一个字符串,“wren”实际表示的是字符串的地址
//因此这条语句将“wren”的地址赋值给了bird指针
const char *bird = "wren";
char *ps; // 未初始化
cout << animal << " and ";
cout << bird << "\n";
// cout << ps << "\n"; //may display garbage, may cause a crash
cout << "Enter a kind of animal: ";
cin >> animal; // ok if input < 20 chars
// cin >> ps; Too horrible a blunder to try; ps doesn't
// point to allocated space
//这两个指针指向用一块内存和字符串
ps = animal; // 注意将animal赋给ps并不会复制字符串,只是复制地址。
cout << ps << "!\n"; // ok, same as using animal
cout << "Before using strcpy():\n";
//因为是char*类型。所以需要强转为指针的地址类型
cout << animal << " at " << (int *) animal << endl;
cout << ps << " at " << (int *) ps << endl;
//创建新的内存空间
ps = new char[strlen(animal) + 1]; // get new storage
//第一个参数是目标地址,第二个参数是要复制字符串的地址
strcpy(ps, animal); // 将字符串复制到新存储
cout << "After using strcpy():\n";
cout << animal << " at " << (int *) animal << endl;
cout << ps << " at " << (int *) ps << endl;
delete[] ps;
// cin.get();
// cin.get();
return 0;
}
/*output
bear and wren
Enter a kind of animal: dog
dog!
Before using strcpy():
dog at 0xad4c9ffd40
dog at 0xad4c9ffd40
After using strcpy():
dog at 0xad4c9ffd40
dog at 0x28c890018d0
*/
C-风格字符串比较
C++将C-风格字符串视为地址,所以使用关系运算符比较,无法得到满意的结果。
需要使用strcmp()函数来比较。
1.该函数接受两个字符串地址作为参数,这意味着参数可以是指针,字符串常量或者是字符数组名。
2.如果第一个字符串按字母顺序排在第二个字符串之前, 则strcmp()将返回一个负数值;
3.如果第一个字符串按字母顺序排在第 二个字符串之后, 则strcpm()将返回一个正数值。
使用关系运算符可以比较字符,因为字符实际上是整数。例如:
for (ch = 'a'; ch <= 'z'; ch++)
cout << ch;
例如,比较单词:
int main(void) {
using namespace std;
char word[5] = "?ate";
for (char ch = 'a'; strcmp(word, "mate"); ch++) {
cout << word << endl;
word[0] = ch;
}
cout << "After loop ends, word is 11" << word << endl;
return 0;
}
/*
?ate
aate
bate
cate
date
eate
fate
gate
hate
iate
jate
kate
late
After loop ends, word is 11mate
*/
7.比较string类字符串
int main() {
using namespace std;
string word = "?ate";
for (char ch = 'a'; word != "mate"; ch++) {
cout << word << endl;
word[0] = ch;
}
cout << "After loop ends, word is " << word << endl;
// cin.get();
return 0;
}
因为string类重载了!= 所以可以这样比较。
函数使用指针处理数组
1.C++将数组名解释其第一个元素的地址:
cookies == &cookies[0];
2.如果将取地址符&用于数组名时,将返回整个数组的地址。
3.在c++中,当且仅当用于函数头或函数原型中,int * arr和int arr [] 的含义才是相同的。都意味着arr是一个int指针。
4.数组表示法 int arr []提醒用户,arr不仅指向int,还指向int数组的第一个int
请记住这个恒等式:
arr[i] == *(ar + i)
&arr[i] == ar + i
将指针(包括数组名) 加1’实际上是加上了一个与指针指向的类型的长度(以字节为单位)
相等的值。对于遍历数组而言, 使用指针加法和数组下标时等效的。
char *buildstr(char c, int n); // prototype
int main() {
using namespace std;
int times;
char ch;
cout << "Enter a character: ";
cin >> ch;
cout << "Enter an integer: ";
cin >> times;
char *ps = buildstr(ch, times);
cout << ps << endl;
delete[] ps; // free memory
ps = buildstr('+', 20); // reuse pointer
cout << ps << "-DONE-" << ps << endl;
delete[] ps; // free memory
// cin.get();
// cin.get();
return 0;
}
// builds string made of n c characters
char *buildstr(char c, int n) {
char *pstr = new char[n + 1];
pstr[n] = '\0'; // terminate string
while (n-- > 0)
pstr[n] = c; // fill rest of string
return pstr;
}
/*output
Enter a character: z
Enter an integer: 10
zzzzzzzzzz
++++++++++++++++++++-DONE-++++++++++++++++++++
*/
函数指针基础使用
函数也有地址。函数的地址是存储其机器语言代码的内存的开始地址。
可以编写将另一个函数的地址作为参数的函数。 这样第一个函数将能够找到第二个函数, 并运行它。 与直接调用另一个函数相比,这种方法很笨拙, 但它允许在不同的时间传递不同函数的地址, 这意味着可以在不同的时间使用不同的函数。
1.获取函数的地址
使用函数名即可。(后面没有参数)例如:think()是一个函数,则think就是该函数的地址。
2.声明函数指针
我们在声明指向某种数据类型的指针时,必须指定指针指向的类型。
声明指向函数的指针时,也必须指定指针指向的函数类型。这意味着声明应指定函数的返回类型以及函数的特征标(参数列表)。也就是说, 声明应像函数原型那样指出有关函数的信息。
double pam(int);
double (*pf)(int);//通常,要声明指向特定类型的函数的指针, 可以首先编写这种函数的原型, 然后用(*pf)替换 函数名。 这样pf就是这类函数的指针。
pf = pam; //注意, pam()的特征标和返回类型必须与pf 相同
double x = pam(4);
double y = (*pf)(5);
//-------------------------------
double y2 = pf(5);
//上面使用pf和(*pf)等价。
//一种认为pf时函数指针,而*pf是函数,因此将(*pf)()用作函数调用。
//一种认为函数名是指向该函数的指针,指向函数的指针的行为应该和函数名相似。
怎么理解?
为了提供正确的运算符优先级,必须在声明中使用括号将pf括起。
括号的优先级比 * 运算符高,因此pf(int)意味着pf()是一个返回指针的函数,而(*pf)(int)意味着pf 是一个指向函数的指针。
typedef关键字
C++为类型建立别名的方式有两种。
一种是预处理器:
#define BYTE char// preprocessor replaces BYTE with char
这样, 预处理器将在编译程序时用 char 替换所有的 BYTE,从而使 BYTE 成为 char 的别名。
笫二种方法是使用 C++(和 C) 的关键字 typedef 来创建别名。
例如要将 byte 作为 char 的别名,可以这样做:
typedef char byte; / / makes byte an alias for char
下面是通用格式:
typedef typeName aliasName;
换句话说, 如果要将 aliasName 作为某种类型的别名, 可以声明 aliasName, 如同将 aliasName 声明为 这种类型的变量那样, 然后在声明的前面加上关键宇 typedef。例如,要让 byte_pointer 成为 char 指针的别名, 可将 byte_pointer 声明为 char 指针, 然后在前面加上 typedef:
typedef char* byte_pointer;// pointer to char type
注意:某些场景下使用#define,不适用。例如:
#define FLOAT_POINTER float *
FLOAT_POINTER pa,pb;//只有pa是float指针变量,pb只是float类型
上面代码使用typedef方法不会有这样的问题。typedef不会创建新类型,而只是为已有的类型建立一个新的名称。
使用typedef简化函数指针
typedef double const double *(*p_fun)(const double *,int);
p_fun p1 = f1;//p1 points to the f1() function
虚函数的工作原理
使用virtual的原因是:希望同一个方法在派生类和基类中的行为不同。
其中有2种机制来实现:
1.在派生类中重新定义基类的方法
2.使用虚方法
通常, 编译器处理虚函数的方法是: 给每个对象添加一个隐藏成员。 隐藏成员中保存了一个指向函数
地址数组的指针。这种数组称为虚函数表 (virtual function table, vtbl)。虚函数表中存储了为类对象进行声
明的虚函数的地址。
例如,
1.基类对象包含一个指针, 该指针指向基类中所有虚函数的地址表。
2.派生类对象将包含一个指向独立地址表的指针。
3.如果派生类提供了虚函数的新定义, 该虚函数表将保存新函数的地址;
4.如果派生类没有重新定义虚函数, 该 vtbl 将保存函数原始版本的地址。
5.如果派生类定义了新的虚函数,则该函数的地址也将被添加到vtbl中。
上图解释:
对象A有两个虚函数vfunc1() 和vfunc2(),对象B复写了vfunc1(),并且有自己的func2() 对象C 也覆写了vfunc1()。
总结一下:
1 在基类方法的声明中使用关键字virtual可使该方法在基类以及所有的派生类(包括从派生类派生 出来的类) 中是虚的。
2 如果使用指向对象的引用或指针来调用虚方法, 程序将使用为对象类型定义的方法, 而不使用为引用或指针类型定义的方法。 这称为动态联编或晚期联编。 这种行为非常重要, 因为这样基类指 针或引用可以指向派生类对象。
3 如果定义的类将被用作基类, 则应将那些要在派生类中重新定义的类方法声明为虚的。
4 如果派生类没有重新定义函数, 将使用该函数的基类版本。 如果派生类位于派生链中, 则将使用最新的虚函数版本, 例外的情况是基类版本是隐藏的。
const使用
decltype
decltype(f())sum = x;//sum的类型就是函数f的返回类型
它的作用是选择并返回操作数的数据类型。在这个过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值。
需要注意以下几点:
1.如果decltype使用的表达式是一个变量,则decltype返回该变量的类型。(包括顶层const和引用在内。)
const int ci= 0, &cj = Cl;
decltype(ci)x = 0;//x的类型是const int
decltype(cj)y =x;//y的类型是const int&,y绑定到变量x
decltype(cj)z;//错误z是一个引用,必须初始化
2.如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型。
3.有些表达式将向decltype返回一个引用类型。一般来说,意味着该表达式的结果对象能作为一条赋值语句的左值。
int i= 42;
int* p = &i;
int &r = i;
//r是一个引用,因此decltype(r)的结果是引用类型。r+0是一个具体的值
decltype(r+0)b;//加法的结果是int 因此b是一个为初始化的int.
//表达式的操作时解引用,则decltype将得到引用类型。
decltype(*p)c;//错误的!!!
解引用指针可以得到指针所指的对象。而且还能给这个对象赋值。
4.decltype ((variable))(注意是双层括号)的结果永远是引用,而 decltype(variable)结果只有当variable本身就是一个引用时才是引用。
#include <iostream>
#include <type_traits>
struct A { double x; };
const A* a;
decltype(a->x) y; // y 的类型是 double(其声明类型)
decltype((a->x)) z = y; // z 的类型是 const double&(左值表达式)
template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) // 返回类型依赖于模板形参
{ // C++14 开始可以推导返回类型
return t+u;
}
int main()
{
int i = 33;
decltype(i) j = i * 2;
std::cout << "i = " << i << ", "
<< "j = " << j << '\n';
std::cout << "i 和 j 的类型相同吗?"
<< (std::is_same_v<decltype(i), decltype(j)> ? "相同" : "不同") << '\n';
auto f = [](int a, int b) -> int
{
return a * b;
};
decltype(f) g = f; // lambda 的类型是独有且无名的
i = f(2, 2);
j = g(3, 3);
std::cout << "i = " << i << ", "
<< "j = " << j << '\n';
}
/*output
i = 33, j = 66
i 和 j 的类型相同吗?相同
i = 4, j = 9
*/
模板别名using
用法:
对命名空间的 using 指令及对命名空间成员的 using 声明.
void f();
namespace A
{
void g();
}
namespace X
{
using ::f; // 全局 f 现在作为 ::X::f 可见
using A::g; // A::g 现在作为 ::X::g 可见
using A::g, A::g; //(C++17)OK:命名空间作用域允许双重声明
}
void h()
{
X::f(); // 调用 ::f
X::g(); // 调用 A::g
}
对类成员的 using 声明.
#include <iostream>
// 基类 B
struct B
{
virtual void f(int) { std::cout << "B::f\n"; }
void g(char) { std::cout << "B::g(char)\n"; }
void h(int) { std::cout << "B::h\n"; }
protected:
int m; // B::m 是受保护的
typedef int value_type;
};
// 派生类 D
struct D : B
{
using B::m; // D::m 是公开的
using B::value_type; // D::value_type 是公开的
// using 的非必要使用场合:
// B::f 已经公开,它作为 D::f 公开可用,
// 并且下面的 D::f 和上面的 B::f 有相同的签名。
using B::f;
void f(int) { std::cout << "D::f\n"; } // D::f(int) **覆盖** B::f(int)
// using 的必要使用场合:
// B::g(char) 被 D::g(int) 隐藏,
// 除非它在这里通过 using B::g 显式暴露为 D::g(char)。
// 如果不用 using B::g,将 char 传递给 D::g(char) 实际上调用的是下面定义的 D::g(int),
// 因为后者隐藏了 B::g(char),并且 char 形参会因此隐式转型到 int。
using B::g; // 将 B::g(char) 作为 D::g(char) 暴露,
// 后者与下面定义的 D::g(int) 完全是不同的函数。
void g(int) { std::cout << "D::g(int)\n"; } // g(int) 与 g(char) 均作为 D 的成员可见
// using 的非必要使用场合:
// B::h 已经公开,它作为 D::h 公开可用,
// 并且下面的 D::h 和上面的 B::h 有相同的签名。
using B::h;
void h(int) { std::cout << "D::h\n"; } // D::h(int) **隐藏** B::h(int)
};
int main()
{
D d;
B& b = d;
// b.m = 2; // 错误:B::m 受保护
d.m = 1; // 受保护的 B::m 可以作为公开的 D::m 访问
b.f(1); // 调用派生类的 f()
d.f(1); // 调用派生类的 f()
std::cout << "----------\n";
d.g(1); // 调用派生类的 g(int)
d.g('a'); // 调用基类的 g(char),它**只是因为**
// 派生类中用到了 using B::g; 才会暴露
std::cout << "----------\n";
b.h(1); // 调用基类的 h()
d.h(1); // 调用派生类的 h()
}
/*output
D::f
D::f
----------
D::g(int)
B::g(char)
----------
B::h
D::h
*/
类型别名与别名模板声明 (C++11 起).
#include <string>
#include <ios>
#include <type_traits>
// 类型别名,等同于
// typedef std::ios_base::fmtflags flags;
using flags = std::ios_base::fmtflags;
// 名字 'flags' 现在指代类型:
flags fl = std::ios_base::dec;
// 类型别名,等同于
// typedef void (*func)(int, int);
using func = void (*) (int, int);
// 名字 'func' 现在指代函数指针:
void example(int, int) {}
func f = example;
// 别名模板
template<class T>
using ptr = T*;
// 名字 'ptr<T>' 现在是指向 T 的指针的别名
ptr<int> x;
// 用于隐藏模板形参的别名模版
template<class CharT>
using mystring = std::basic_string<CharT, std::char_traits<CharT>>;
mystring<char> str;
// 别名模板可引入成员 typedef 名
template<typename T>
struct Container { using value_type = T; };
// 可用于泛型编程
template<typename ContainerType>
void g(const ContainerType& c) { typename ContainerType::value_type n; }
// 用于简化 std::enable_if 语法的类型别名
template<typename T>
using Invoke = typename T::type;
template<typename Condition>
using EnableIf = Invoke<std::enable_if<Condition::value>>;
template<typename T, typename = EnableIf<std::is_polymorphic<T>>>
int fpoly_only(T t) { return 1; }
struct S { virtual ~S() {} };
int main()
{
Container<int> c;
g(c); // Container::value_type 将在此函数中是 int
// fpoly_only(c); // 错误:被 enable_if 禁止
S s;
fpoly_only(s); // OK:被 enable_if 允许
}
explicit关键字
指定构造函数或转换函数 (C++11 起)或推导指引 (C++17 起)为显式,即它不能用于隐式转换和复制初始化。
声明时不带函数说明符 explicit 的拥有单个无默认值形参的 (C++11 前)构造函数被称作转换构造函数。
struct A
{
A(int) { } // 转换构造函数
A(int, int) { } // 转换构造函数(C++11)
operator bool() const { return true; }
};
struct B
{
explicit B(int) { }
explicit B(int, int) { }
explicit operator bool() const { return true; }
};
int main()
{
A a1 = 1; // OK:复制初始化选择 A::A(int)
A a2(2); // OK:直接初始化选择 A::A(int)
A a3 {4, 5}; // OK:直接列表初始化选择 A::A(int, int)
A a4 = {4, 5}; // OK:复制列表初始化选择 A::A(int, int)
A a5 = (A)1; // OK:显式转型进行 static_cast
if (a1) ; // OK:A::operator bool()
bool na1 = a1; // OK:复制初始化选择 A::operator bool()
bool na2 = static_cast<bool>(a1); // OK:static_cast 进行直接初始化
// B b1 = 1; // 错误:复制初始化不考虑 B::B(int)
B b2(2); // OK:直接初始化选择 B::B(int)
B b3 {4, 5}; // OK:直接列表初始化选择 B::B(int, int)
// B b4 = {4, 5}; // 错误:复制列表初始化不考虑 B::B(int,int)
B b5 = (B)1; // OK:显式转型进行 static_cast
if (b2) ; // OK:B::operator bool()
// bool nb1 = b2; // 错误:复制初始化不考虑 B::operator bool()
bool nb2 = static_cast<bool>(b2); // OK:static_cast 进行直接初始化
}