Массивы |
Массив является одним из ключевых понятий программирования. Итак, массив - это несколько значений одного типа, расположенных в памяти рядом. Зная местоположение первого из этих элементов, можно обратиться к любому из них. Порядковый номер элемента в массиве называется индексом.
Массивы - это та часть языка Си, которая не подверглась изменениям при эволюционировании языка в C++. Поэтому их объявление и работа с ними на обоих этих языках совпадает. Чтобы создать в памяти новый массив, используется такая запись:
int m[10];
int - это тип элементов массива, одинаковый для всех них. Конечно, Вы можете использовать любой другой тип (кроме void), чтобы задавать массивы. Квадратные скобки обозначают, что это массив, целое число в скобках (обязательно константа, известная на момент компиляции) - его размер, т.е. количество элементов. m - это имя нашей переменной-массива. Важно заметить, что в C/C++ типы "массив" и "указатель" тесно связаны, поэтому в функцию, которая требует указатель, вполне законно передавать массив:
void MyFunc( int *arr ); MyFunc(m);
С другой стороны, среди формальных параметров функции можно объявить и обычный массив, и даже массив без указания размера. Компилятору это все равно, он в любом случае создаст функцию, принимающую указатель на начало массива. И уж точно массив не будет копироваться в параметры функции, если Вы напишете что-нибудь вроде:
int arr[45] void MyFunc( int *arr ); // компилятор посчитает void MyFunc( int arr[] ); // эти три объявления void MyFunc( int arr[15] ); // абсолютно идентичными
Итак, ещё раз: в языках Си и C++ типы "указатель" и "массив" очень похожи. Разница лишь в том, что при создании массива происходит автоматическое выделение памяти, в то время как вместе с указателем никакой дополнительной памяти не выделяется. При вызове функций массивы и указатели взаимозаменяемы.
Но при всём том, массив и указатель — это различные типы. Так, если написать в одном файле
int a[10];
а в другом
extern int *a;
- компилятор, скорее всего, выдаст Вам ошибку.
Если создаётся глобальный массив, то изначально все его элементы по умолчанию нули, однако зачастую возникает необходимость присвоить им другие начальные значения. Процесс присваивания начальных значений и называется инициализацией. В C/C++ инициализация осуществляется с помощью знака =, который пишется после имени переменной. Этот знак не есть оператор присваивания, поэтому если Вы будете таким образом инициализировать экземпляры классов, то будет вызван конструктор, а не функция operator=.
double dbl = 1.0; /* инициализация простой переменной */ double *m[5] = {NULL, NULL, &dbl}; /* инициализация массива указателей */
Как видно из данного примера, значения для инициализации массива пишутся через запятую, причём вся группа значений берётся в фигурные скобки. Если указано n значений для инициализации, и n меньше числа элементов в массиве, то первые n элементов инициализируется согласно списку значений, а остальные эементы становятся нулями. То есть, массив m после инициализации будет выглядеть таким образом: NULL(0), NULL(0), &dbl, 0(NULL), 0(NULL).
Кроме указанного способа, массив символов можно инициализировать непосредственно с помощью строкового литерала:
char str[10] = "Aspid";
Но при этом надо помнить, что длина строки на самом деле на единицу больше, чем видимое число символов в ней, поскольку она содержит ещё и символ завершающего нуля.
Наконец, инициализация массива позволяет избежать явного объявления размера. Массив автоматически будет создан такого размера, сколько элементов содержится в списке инициализации:
int m[] = {2, 4, 6}; /* создаётся массив из трёх элементов */ char c[] = "Sample String"; /* создаётся массив содержащий строку, размерность подсчитывается автоматически */
Предположим, у нас имеется массив m (или, что то же самое, указатель на его начало). Как нам обратиться к самому первому его элементу? Ко второму? К (k + 1)-му? Правильно, так:
*m *(m + 1) *(m + k)
Вот это число, которое прибавляется к указателю m, и есть индекс элемента в массиве. В языках C/C++ индексация массива начинается с нуля, поэтому самый первый элемент массива всегда имеет индекс 0. К счастью, язык предоставляет гораздо более удобное средство обращения к элементу с индексом k, а именно, квадратные скобки:
m[k] = 17;
В данном примере в ячейку с индексом k записывается число 17. Существует так же альтернативный способ записи, который приводит к ровно такому же результату. Возможность такой записи вытекает из коммутативности сложения указателя и целого числа. Вот он, этот альтернативный способ:
k[m] = 17; 0[&x] = x + x; // контрольный вопрос : что делает эта строчка?
Правда, я ещё ни разу не видел, чтобы такая экзотическая запись где-нибудь использовалась.
Конечно, ничто не в силах нам запретить обратиться к элементу 20-элементного массива с индексом 138, равно как и к минус первому элементу. Всё дело в том, что язык не располагает встроенными средствами проверки выхода за границы массива. Предполагается, что пользователь должен самостоятельно следить за тем, чтобы не вылезти за границы массива.
Кстати, если массив m объявлен в программе где-то далеко, то вы можете "на ходу" узнать количество элементов в нём в помощью вот такой конструкции:
sizeof(m) / sizeof(m[0])
На самом деле, этот метод предпочтительнее, чем прямое указание размера, потому что если вдруг размер массива нужно изменить, то переписать нужно всего одно число при объявлении, вместо того, чтобы ползать по коду, выискивая места, где ещё понадобилось это значение.
Основное удобство при работе с массивами заключается в том, что с ними можно работать посредством циклов, а не обращаясь к каждому элементу по отдельности
int* a=new int [10]; for(int i=0;i<10;i++) { a[i]=random(25); } //... delete []a;
Одна из самых приятных особенностей языка - возможность создавать массив из массивов (т.н. двухмерные массивы), из таких массивов собрать ещё один массив (тогда получится трёхмерный массив) и т.д. Интуитивно понятно, как это делается:
int m[5][8];
Такой код генерирует массив из 5 элементов, каждый из которых является массивом из 8 элементов типа int. Можно обратиться к любому из 5 подмассивов непосредственно (m[3]), либо к конкретному элементу конкретного подмассива (m[3][7]). Первый случай, естественно, проходит только для чтения, поскольку m[3], на самом деле, не является LValue, т.е. объектом, имеющим физический адрес.
При написании серьезных проектов всегда возникает необходимость выделить дополнительный кусок памяти. Динамическая память - это отнюдь не "барство дикое", а необходимый инструмент. Просто зачастую (например, если мы описываем деревья или списки) изначально нам неизвестно, сколько ячеек памяти может понадобиться. На самом деле, такая проблема была на всем протяжении существования науки/искусства программирования, поэтому неудивительно, что ещё в Си были функции для динамической работы с памятью.
В Си для этого было две главных функции: одна называлась malloc() и выделяла непрерывный кусок памяти, другая, free(), этот кусок освобождала. Вот как выглядит код на Си для работы с динамической памятью (она на жаргоне называется кучей, heap):
int *Piece; Piece = (int*)malloc(sizeof(int)); /* аргумент функции malloc() - число байт, которые надо выделить */ if (Piece == NULL) /* malloc() возвращает NULL, если не может выделить память */ { printf("Ошибка выделения памяти: видимо, недостаточно места в ОЗУ\n") return; } . . . free(Piece); /* аргумент free() - указатель на уже ненужный кусок памяти */
Если возникала необходимость выделить память под несколько переменных одного типа, расположенных рядом (то есть под массив), аргумент malloc()'а просто домножали на нужное количество ячеек массива:
int *Piece = (int*)malloc(15 * sizeof(int));
Был, правда, у malloc()'а один "недостаток": выделяя память, он не изменял содержимое ячеек, поэтому там могло оказаться совершенно произвольное значение. С этим боролись либо с помощью специальной функции memset(ptr, c, n) (она заполняет n байт памяти начиная с места, на которое указывает ptr, значением c), либо с помощью calloc()'а. Функция calloc() принимает два параметра: число ячеек массива, под которые надо выделить память, и размер этой ячейки в байтах; делает эта функция следующее: выделяет нужное количество памяти (непрерывный кусок) и обнуляет все значения в нём. Таким образом такой код:
int *Piece = (int*)malloc(15 * sizeof(int)); memset(Piece, 0, 15 * sizeof(int));
эквивалентен такому:
int *Piece = (int*)calloc(15, sizeof(int));
Идеология языка C++ предполагает, что каждый объект создаётся (объявляется) именно в том месте, где он нужен, и является работоспособным сразу после создания. Для этого каждый класс имеет определёный набор конструкторов - функций, которые должны автоматически запускаться при создании объекта (экземпляра данного класса) и инициализировать его члены (data members). Конструкторы одного класса отличаются только количеством и типом передаваемых параметров, то есть явяются перегруженными функциями. Однако, к сожалению, функции malloc() и сalloc() не умеют автоматически запускать конструкторы, и потому непригодны для динамического создания объектов. В языке C++ им имеется адекватная замена - оператор new. Рассмотрим пример:
MyClass *mc = new MyClass(5);
В данном случае создаётся экземпляр класса MyClass, после чего с помощью его конструктора, принимающего в качестве параметра целое число (в данном случае, число 5), объект "инициализируется" этим числом. Адрес вновь созданного объекта присваивается указателю mc. Если для класса определён конструктор по умолчанию, после имени класса допускается не указывать пустые скобки. Писать их или нет - это, как говорится, дело вкуса:
new MyClass(); // эти две строки кода new MyClass; // абсолютно эквивалентны
Естественно, ничто не мешает использовать оператор new для простых скалярных переменных (например, целых чисел или других указателей).
Важное отличие оператора new от функции malloc() заключается в том, что он возвращает значение типа "указатель-на-объект" (то есть MyClass *), в то время как функция malloc() - "указатель-на-что-угодно" (void *). Подобная типизация в C++ - не редкость, она строже, чем та, что используется в Си, и, следовательно, менее ошибкоопасна. Извратившись, и в C++ можно скомпилировать код, где указатель на один класс приводится к указателю на другой класс, никак не связанный с первым — но в C++ это можно сделать только специально.
Для каждого класса, помимо конструкторов, определён ещё и деструктор, то есть функция, отвечающая за корректное уничтожение объекта. Деструктор никогда никаких параметров не принимает, и потому не может быть перегружен. Проблема с деструктором возникает та же, что и с конструктором: функция free() не умеет его вызывать. Поэтому в C++ введён ещё один оператор - delete. Синтаксис его очень прост:
delete mc;
где mc - указатель на класс. Именно для этого класса и вызовется деструктор, поэтому, если Вы объявили его как "указатель-на-что-угодно", деструктор не будет вызван вообще. Собственно, именно поэтому void * не рекомендуется использовать. Другой пример:
class Base { . . . }; class Derived : public Base { . . . }; int main( void ) { Base *ptr = new Derived; // присваивать указателю на предка адрес потомка - можно, но почти всегда требует виртуального деструктора<!--тут надо бы ссылку внутри книги --> . . . delete ptr; return 0; }
В этом случае оператором delete вызовется деструктор базового класса Base, хотя требуется вызвать деструктор класса-потомка Derived. Казалось бы, применение RTTI (Run-Time Type Info) в среде Microsoft Visual Studio позволило бы спастись от этой напасти, но увы и ах... В принципе, гибким решением этой пробемы является применение виртуальных деструкторов. Вообще же, как мы видим, при использовании delete надо проявлять особую осторожность.
|
Метки: массивы |
Указатели и динамическая память |
Часто размер используемого при решении задачи массива не задан на момент компиляции программы, а определить его можно только при выполнении программы. В этом случае используется механизм, называемый динамическим распределением памяти. Для использования динамического распределения памяти необходимо создать переменную специального типа, называемую указателем, затем создать в памяти массив необходимого размера, доступ к которому будет осуществляться через этот указатель, наконец, освободить выделенную память после завершения работы с массивом.
Указатель — это переменная специального типа. Она хранит не какое-то числовое значение, а адрес (номер первого байта в памяти компьютера), по которому хранится какая-то другая переменная. При создании указателя необходимо задать тип переменной, на которую он указывает. Синтаксис объявления указателя такой:
имя_типа * идентификатор;
Пример:
int * pi;
float * pf, f;
double * ps, * pt;
В первой строке этого примера объявлены переменная pi, являющейся указателем на тип int (то есть в ячейке памяти, на которую указывает pi должна хранится переменная типа int). Во второй строке объявлены переменная pf, являющейся указателем на тип float и переменная f типа float. Обратите особое внимание на эту строчку: для того, чтобы объявить несколько указателей в одной строке, необходимо перед идентификатором каждого из них поставить символ *. А еще лучше объявлять в одной строке только одну переменную. В третей строке объявляется два указателя на тип double: ps и pt.
Сразу после объявления значение указателя не определено, то есть он может указывать в произвольную ячейку памяти, поэтому пользоваться им нельзя. Иначе вы в лучшем случае получите ошибку segmentation fault, в худшем — программа будет работать непредсказуемо.
Для того, чтобы создать в памяти новую переменную используется оператор new. Это унарный оператор, единственный операнд которого должен быть именем типа создаваемой переменной. Оператор new возвращает адрес вновь созданной переменной, который можно присвоить указателю. Например:
int * pi; // Объявление переменной pi
pi=new int; // Выделение памяти для переменной pi
Для доступа к ячейке памяти, адрес которой хранится в указателе, необходимо использовать унарный оператор разыменования, записываемый символом *. Пример:
*pi=5; // *pi - разыменование указателя pi
cout<<(*pi); // Будет напечатано 5
В этом примере ячейке, на которую указывает pi сначала присваивается число 5, а потом значение этой ячейки памяти выводится на экран. Присвоить числовое значение непосредственно указателю (pi=5) нельзя!
Также указателю можно присвоить адрес уже существующей переменной, для этого к переменной нужно применить унарный оператор взятия адреса & :
int a, b, *p; // a и b имеют тип int, p - указатель на int
a=3;
p=&a; // Теперь p указывает на a
cout<<*p; // Будет выведено 3
p=&b; // Теперь p указывает на b
*p=7; // В результате будет изменено значение b
cout<<b; // Будет выведено 7
newДля создания массива в динамической памяти используется оператор new с указанием размера создаваемого массива. Пример:
int * pi; // Объявить указатель pa
pi=new int[n]; // Создать массив из n элементов типа int
Теперь с pi можно работать, как с обычным массивом из n элементов типа int: становятся доступны элементы pi[0], pi[1], ..., pi[n-1].
newСуществует единственное числовое значение, которое можно присвоить непосредственно указателю: это 0 (то есть присваивание pi=0 разрешено, а присваивание pi=1 — нет). Нулевой адрес — особый, по этому адресу не может хранится ни одна переменная. То есть указатель, имеющий нулевое значение указывает в "никуда", к такому указателю нельзя применить оператор разыменования.
Оператор new использует функцию операционной системы для выделения памяти. Если затребованный размер памяти слишком большой (а также при попытке создать массив из нуля или отрицательного числа элементов), операционная система не будет выделять память и оператор new вернет нулевое значение. Если это нулевое значение будет присвоено указателю, к которому впоследствии будет применен оператор разыменования или оператор обращения к элементу массива, то программа аварийно завершит работу с ошибкой segmentation fault. Чтобы быть уверенным, что оператор new был выполнен удачно, необходимо сразу после его вызова проверить значение, которое он вернул и в случае, если оно равно 0, выполнить какие-либо особенные действия, например, вывести сообщение о невозможности выделения необходимого объема памяти. Например:
int n=1000000000;
int *pi=new int[n];
if(pi==0)
{
cout<<"Невозможно создать массив из "<<n<<" элементов int";
return 1; // Завершаем работу функции main
}
После окончания работы с массивом, когда выделенная ранее память перестанет быть нужной, ее необходимо освободить, чтобы дать возможность операционной системе использовать эту память по своему усмотрению, например, выделить другой программе. Для этого используется унарный оператор delete, единственный операнд которого — адрес, по которому начинается память, ранее выделенная оператором new, которую мы хотим освободить. Например:
delete pi;
При этом сам указатель pi не уничтожается, ему можно присвоить новое значение, однако разыменование этого указателя может привести к ошибке.
Теперь объединим все вышесказанное в одну программу, создающую в динамической памяти массив типа double, заполняющую его данными и уничтожающую в конце.
int n; // Размер массива
double * p; // Указатель на начало массива
cout<<"Введите размер массива: ";
cin>>n;
p=new double[n]; // Выделяем память
if(p==0) // Проверка успешности выделения памяти
{
cout<<"Невозможно выделить память"<<endl;
return 1; // Завершаем работу
}
for(int i=0;i<n;++i) // Цикл для считывания массива
cin>>p[i]; // Считали i-й элемент массива
//// Теперь выполняем что-нибудь с массивом
delete p; // Освобождаем память
Указателям можно присваивать значение, являющееся указателем того же типа (которое может быть результатом оператора new, оператора & или другим указателем того же типа). К указателям можно применять оператор разыменования *. Кроме этого с указателями можно выполнять ряд других операций. Далее мы предполагаем, что p и q — указатели одного типа, например, объявленные как int *p, *q.
Как и числа, указатели можно сравнивать между собой.
p==qp!=qp<qtrue, если ячейка, на которую указывает p находится в памяти раньше, чем ячейка, на которую указывает q. Аналогично определяются сравнения p<=q, p>q, p>=q Как всегда, нельзя путать операторы проверки на равенство p==q и присваивания p=q. В результаты выполнения оператора присваивания p будет указывать туда же, куда и q, значения же ячеек памяти, на которые указывали p и q не изменятся.
К указателям можно прибавлять и вычитать целые числа. Пусть p указывает на начало массива. Тогда p+0 равно p, p+1 — это указатель на следующий элемент массива, то есть &p[1], и для любого положительного i p+i — это указатель на p[i]. Вычитание из указателя 1 (а также прибавление к указателю -1) возвращает указатель на переменную, которую можно разместить в памяти непосредственно перед переменной, на которую указывает p.
Если p указывает не на начало массива, а на произвольную ячейку памяти, к значению p можно прибавлять целые числа, при этом результат будет такой же, как описано выше.
Также к указателям можно применять оператор доступа к элементам массива []: p[0] при этом означает переменную, хранящуюся в ячейке памяти, на которую указывает p, p[1] — следующую за ней, а p[-1] — предшествующую ей и т.д.
Поскольку определены операторы сложения и вычитания указателя с целыми числами, то указателям можно применять операции инкремента ++, декремента --, а также увеличивать (+=) и уменьшать -= значения указателей на целые значения.
Наконец, два указателя одного типа можно вычитать друг из друга. В этом случае оператор вычитания p-q возвращает целое число, при прибавлении которого к указателю q получается указатель p.
Никакие другие операторы к указателям применять нельзя. В частности, указатели нельзя складывать, умножать, умножать указатель на число и т.д.
Итак, с указателем можно работать, как с массивом, в частности, к нему можно применять оператор доступа к элементу []. Верно и обратное — с массивом можно работать, как с указателем. Если мы объявили массив int arr[10], то мы можем использовать идентификатор arr без указания элемента массива, как синоним для указателя на начало массива. С массивами можно выполнять все операции, которые можно делать с указателями, кроме тех, которые меняют значение самого указателя: =, +=, -=, ++. --, то есть единственное отличие массивов от указателей заключается в том, что значение указателя можно изменить в программе, а значение массива (то есть адрес его начала) — нет.
|
|
Различные функции в стиле С |
abort, Прерывает выполнение пргораммы
abs, Возвращает модуль числа
atexit, Регистрирует функцию, вызываемую при завершении работы программы
atof, Преобразует строку в вещественное число
atoi, Преобразует строку в целое число
atol, Преобразует строку в длинное целое число
bsearch, Ищет элемент в отсортированном массиве
calloc, Выделяет блок памяти
div, Делит с остатком
exit, Прерывает выполнение программы
free, Освобождает блок памяти
getenv, Возвращает значение переменной окружения
labs, Возвращает модуль числа
ldiv, Делит с остатком
malloc, Выделяет блок памяти
qsort, Сортирует данный массив
rand, Генерирует случайные числа
realloc, Измеряет размеры ранее выделенного блока памяти
srand, Устанавливает начальное псевдослучайное число
strtod, Преобразует строку в число
strtol, Преобразует строку в число с учетом системы счисления
strtoul, Преобразует строку в число с учетом системы счисления
system, Передает строку командному процессору ОС
|
Метки: функции |
Проверка на четность/нечетность в C++ |
"a&1" дает истину, когда число нечетное.
Пример проверки четности:
if (a&1) printf("Число нечетно");
else printf("Число четно");
|
Метки: четность нечетность |
Оператор цикла for |
Этот оператор используется, когда необходимо повторить некоторую последовательность действий несколько раз (или ни одного). Подобную задачу можно решить также с помощью другого оператора цикла -
оператора for.
Формат оператора цикла for:
for ( выражение1; выражение2; выражение3 )
оператор;
Выполнение оператора цикла for начинается с вычисления "выражения1". Это своего рода инициализация цикла, которая выполняется только один раз и предшествует последующим действиям.
После этого вычисляется "выражение2". Если оно истинно, то выполняется "оператор". Далее вычисляется "выражение3". Первая итерация цикла закончена. Вторая итерация начинается опять с вычисления "выражения2". Если оно истинно, то выполняется "оператор". Далее опять вычисляется "выражение3" и так продолжается до тех пор, пока "выражение2" будет истинным. Если же уже на первой итерации "выражение2" будет ложным, то цикл не исполнится ни разу.
Если вникнуть в логику работы оператора цикла for, то можно заметить, что она полностью соответствует следующему фрагменту кода с использование оператора цикла while:
выражение1;
while ( выражение2 )
{
оператор;
выражение3;
}
В прошлом выпуске мы рассмотрели пример использования оператора цикла while:
int i = 0;
while ( i < 9 )
{
Print( i );
i++;
}
Print ("Done");
Этот пример можно переписать с использованием оператора цикла for:
int i;
for (i = 0; i < 9; i++)
Print( i );
Print ("Done");
Не будет ошибкой, если любое из трех или все три выражения в операторе цикла for будут отсутствовать, однако разделяющие их точки с запятыми (;) опускать нельзя. Если отсутствует "выражение2", то принимается, что оно всегда равно true.
"Выражение1" и "выражение3" могут состоять из нескольких выражений, объединенных запятой. В этом случае вычисляется каждое из выражение, причем порядок вычисления будет слева направо:
for (i = 0, j = 0; i < 9; i++)
Print ( "i = ", i, " j= ", j );
|
Метки: for |
Google & Windows Mobile |
Лидер поисковых машин интернета, Google занимает более 60% мирового рынка, а значит, шесть из десяти находящихся в сети людей обращаются к его странице в поисках информации в интернете. Сейчас регистрируют ежедневно около 50 миллионов поисковых запросов и индексируют более 8 миллиардов веб-страниц. Кроме поисковой системы, компания Google предоставляет много других бесплатных услуг, в частности популярный почтовый сервис Google Mail, Google Talk. Самым популярным у третьесторонних создателей приложений стал сервис Google Maps. Именно этот сервис лидирует в качестве основы интегрированных приложений. В рамках прочих сервисов, Google возобновил сервис «Вопросы и Ответы».
Недавно, компания Google объявила, что разрабатывает свою собственную платформу для мобильных устройств, под названием Android. Эта платформа, основанная на Linux, разрабатывается Open Handset Alliance. Она позволяет создавать Java приложения, управляющие устройством через разработанные Google библиотеки. Так же есть возможность писать приложения на C и других языках программирования, и компилировать их в ARM код.
Но вот несколько дней назад стало, что Google анонсировал новую операционную систему для компьютеров, названной Google Chrome OS. Google Chrome Operating System — операционная система, первоначально нацеленная на рынок нетбуков. Первые нетбуки, поставляющиеся в комплекте с Chrome OS, ожидаются во второй половине 2010 года. Chrome OS построена на ядре Linux с использованием браузера Google Chrome, работающем в новой, специально разработанной системе окон. Исходный код системы будет открыт в этом году.
Но у компании Google есть небольшой сюрприз для владельцев PDA. Компания Google владеет своим собственным Microsoft Exchange сервером. Он позволяет пользователям PDA и смартфонов на Windows Mobile, синхронизировать календарь и контакты с аккаунтом Google, сохраняя их на сервере. Для этого необходимо настроить встроенную программу ActiveSync.
Для настройки программы на Карманном Персональном Компьютере, выполните следующие действия:
Откройте программу синхронизации ActiveSync.
В Меню выбираете пункт Добавить сервер/Add Server.
Далее в адресе сервера вводите m.google.com. Галочка на против SSL безопасности должна стоять.
В следующем окошке вводите логин и пароль своего аккаунта. Заметьте, акканут должен заканчиваться *@gmail.com.
После появляется список синхронизируемых элементов. Надо выбрать только Календарь и Контакты, остальное еще не реализовано в Google.
На этом настройка ActiveSync закончена. Синхронизация может пройти не сразу, ее надо повторить несколько раз.
По-моему, очень удобная возможность некого «бэкапера», который не требует предустановленного программного обеспечения. Данная возможность, на мой взгляд, будет очень популярна в ближайшем будущем, так как она позволит синхронизировать данные компьютеров под управлением Google Chrome OS и обычных PDA, которые уже имеются у большинства людей.
|
|
| Страницы: [1] Календарь |